From c5759ce854337167e19ab96621c0565bc820e468 Mon Sep 17 00:00:00 2001 From: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> Date: Fri, 11 Sep 2026 11:37:50 +0530 Subject: [PATCH 01/11] test(bedrock): verify environment credentials (#7375) * test(bedrock): verify environment credentials * test(bedrock): isolate credential environment test --- lib/crewai/tests/llms/bedrock/test_bedrock.py | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/crewai/tests/llms/bedrock/test_bedrock.py b/lib/crewai/tests/llms/bedrock/test_bedrock.py index 0127ffd62..87c0d6c09 100644 --- a/lib/crewai/tests/llms/bedrock/test_bedrock.py +++ b/lib/crewai/tests/llms/bedrock/test_bedrock.py @@ -602,24 +602,31 @@ def test_bedrock_tool_conversion(): assert "inputSchema" in bedrock_tools[0]["toolSpec"] -def test_bedrock_environment_variable_credentials(bedrock_mocks): - """ - Test that AWS credentials are properly loaded from environment - """ - mock_session_class, _ = bedrock_mocks +def test_bedrock_environment_variable_credentials(): + """Pass AWS credentials and region from the environment to boto3.""" + with ( + patch.dict( + os.environ, + { + "AWS_ACCESS_KEY_ID": "test-access-key-123", + "AWS_SECRET_ACCESS_KEY": "test-secret-key-456", + "AWS_DEFAULT_REGION": "eu-west-1", + }, + clear=False, + ), + patch( + "crewai.llms.providers.bedrock.completion.Session" + ) as mock_session_class, + ): + mock_session_class.return_value.client.return_value = MagicMock() + LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0") - mock_session_class.reset_mock() - - with patch.dict(os.environ, { - "AWS_ACCESS_KEY_ID": "test-access-key-123", - "AWS_SECRET_ACCESS_KEY": "test-secret-key-456" - }): - llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0") - - assert mock_session_class.called - call_kwargs = mock_session_class.call_args[1] if mock_session_class.call_args else {} - assert call_kwargs.get('aws_access_key_id') == "test-access-key-123" - assert call_kwargs.get('aws_secret_access_key') == "test-secret-key-456" + mock_session_class.assert_called_once_with( + aws_access_key_id="test-access-key-123", + aws_secret_access_key="test-secret-key-456", + aws_session_token=None, + region_name="eu-west-1", + ) def test_bedrock_token_usage_tracking(): From 004f7b58d56bba4291beea96c9a0f79fab0d7d4a Mon Sep 17 00:00:00 2001 From: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> Date: Fri, 11 Sep 2026 19:53:03 +0530 Subject: [PATCH 02/11] test(bedrock): isolate session credential test (#7388) --- lib/crewai/tests/llms/bedrock/test_bedrock.py | 33 ++++--------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/lib/crewai/tests/llms/bedrock/test_bedrock.py b/lib/crewai/tests/llms/bedrock/test_bedrock.py index 87c0d6c09..fd79198fc 100644 --- a/lib/crewai/tests/llms/bedrock/test_bedrock.py +++ b/lib/crewai/tests/llms/bedrock/test_bedrock.py @@ -1,6 +1,4 @@ import os -import sys -import types from unittest.mock import patch, MagicMock import pytest @@ -8,6 +6,7 @@ from crewai.llm import LLM from crewai.crew import Crew from crewai.agent import Agent from crewai.task import Task +from crewai.llms.providers.bedrock import completion as bedrock_completion def _create_bedrock_mocks(): @@ -134,25 +133,6 @@ def test_bedrock_completion_is_used_when_bedrock_provider(): assert llm.model == "anthropic.claude-3-5-sonnet-20241022-v2:0" -def test_bedrock_completion_module_is_imported(monkeypatch): - """ - Test that the completion module is properly imported when using Bedrock provider - """ - module_name = "crewai.llms.providers.bedrock.completion" - - # Restore the original module after this test so collected class references - # still match the provider returned by LLM in subsequent tests. - monkeypatch.delitem(sys.modules, module_name, raising=False) - - LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0") - - assert module_name in sys.modules - completion_mod = sys.modules[module_name] - assert isinstance(completion_mod, types.ModuleType) - - assert hasattr(completion_mod, 'BedrockCompletion') - - def test_native_bedrock_raises_error_when_initialization_fails(): """ Test that LLM raises ImportError when native Bedrock completion fails. @@ -602,8 +582,10 @@ def test_bedrock_tool_conversion(): assert "inputSchema" in bedrock_tools[0]["toolSpec"] -def test_bedrock_environment_variable_credentials(): +def test_bedrock_environment_variable_credentials(monkeypatch): """Pass AWS credentials and region from the environment to boto3.""" + monkeypatch.delenv("AWS_SESSION_TOKEN", raising=False) + with ( patch.dict( os.environ, @@ -614,13 +596,12 @@ def test_bedrock_environment_variable_credentials(): }, clear=False, ), - patch( - "crewai.llms.providers.bedrock.completion.Session" - ) as mock_session_class, + patch.object(bedrock_completion, "Session") as mock_session_class, ): mock_session_class.return_value.client.return_value = MagicMock() - LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0") + llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0") + assert type(llm) is bedrock_completion.BedrockCompletion mock_session_class.assert_called_once_with( aws_access_key_id="test-access-key-123", aws_secret_access_key="test-secret-key-456", From d5c7bac5051ffaf2d03e6ab8da792b737f3e43b3 Mon Sep 17 00:00:00 2001 From: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> Date: Fri, 11 Sep 2026 20:46:05 +0530 Subject: [PATCH 03/11] chore: update OpenRouter tool specifications (#7387) --- lib/crewai-tools/tool.specs.json | 1154 +++++++++++++++++++++++++++++- 1 file changed, 1143 insertions(+), 11 deletions(-) diff --git a/lib/crewai-tools/tool.specs.json b/lib/crewai-tools/tool.specs.json index 674cc33c1..c82d71a65 100644 --- a/lib/crewai-tools/tool.specs.json +++ b/lib/crewai-tools/tool.specs.json @@ -3560,6 +3560,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -3601,6 +3665,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -4619,6 +4686,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -4660,6 +4791,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -6589,6 +6723,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -6630,6 +6828,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -7116,16 +7317,9 @@ "description": "Input for DOCXSearchTool.", "properties": { "docx": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "null" - } - ], "description": "File path or URL of a DOCX file to be searched", - "title": "Docx" + "title": "Docx", + "type": "string" }, "search_query": { "description": "Mandatory search query you want to use to search the DOCX's content", @@ -7134,8 +7328,8 @@ } }, "required": [ - "docx", - "search_query" + "search_query", + "docx" ], "title": "DOCXSearchToolSchema", "type": "object" @@ -8941,6 +9135,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -8982,6 +9240,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -11721,6 +11982,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -11762,6 +12087,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -13027,6 +13355,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -13068,6 +13460,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -14339,6 +14734,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -14380,6 +14839,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -15834,6 +16296,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -15875,6 +16401,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -18322,6 +18851,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -18363,6 +18956,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -20078,6 +20674,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -20119,6 +20779,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -22263,6 +22926,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -22304,6 +23031,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -23768,6 +24498,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -23809,6 +24603,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -25660,6 +26457,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -25701,6 +26562,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -27904,6 +28768,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -27945,6 +28873,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -28963,6 +29894,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -29004,6 +29999,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -30022,6 +31020,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -30063,6 +31125,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, @@ -31081,6 +32146,70 @@ "title": "OpenCLIPProviderSpec", "type": "object" }, + "OpenRouterProviderConfig": { + "description": "Configuration for OpenRouter provider.", + "properties": { + "api_base": { + "title": "Api Base", + "type": "string" + }, + "api_key": { + "title": "Api Key", + "type": "string" + }, + "default_headers": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Headers" + }, + "dimensions": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Dimensions" + }, + "model": { + "title": "Model", + "type": "string" + }, + "model_name": { + "title": "Model Name", + "type": "string" + } + }, + "title": "OpenRouterProviderConfig", + "type": "object" + }, + "OpenRouterProviderSpec": { + "description": "OpenRouter provider specification.", + "properties": { + "config": { + "$ref": "#/$defs/OpenRouterProviderConfig" + }, + "provider": { + "const": "openrouter", + "title": "Provider", + "type": "string" + } + }, + "required": [ + "provider" + ], + "title": "OpenRouterProviderSpec", + "type": "object" + }, "RagToolConfig": { "description": "Configuration accepted by RAG tools.\n\nSupports embedding model and vector database configuration.\n\nAttributes:\n embedding_model: Embedding model configuration accepted by RAG tools.\n vectordb: Vector database configuration accepted by RAG tools.", "properties": { @@ -31122,6 +32251,9 @@ { "$ref": "#/$defs/OpenCLIPProviderSpec" }, + { + "$ref": "#/$defs/OpenRouterProviderSpec" + }, { "$ref": "#/$defs/RoboflowProviderSpec" }, From e1f3c4bdd4d3f065edd560b148763bcd4e008a68 Mon Sep 17 00:00:00 2001 From: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> Date: Fri, 11 Sep 2026 21:02:00 +0530 Subject: [PATCH 04/11] feat: expose CrewAI Platform application catalog (#7383) * feat: expose platform application catalog * refactor: centralize platform application catalog * fix: preserve platform application typing * chore: remove unused platform app export --- .../src/crewai_core/platform_apps.py | 25 +++++++++++++++++++ .../tools/crewai_platform_tools/__init__.py | 3 +++ .../crewai_platform_tools/test_constants.py | 22 ++++++++++++++++ .../crewai/agents/agent_builder/base_agent.py | 20 +-------------- 4 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 lib/crewai-core/src/crewai_core/platform_apps.py create mode 100644 lib/crewai-tools/tests/tools/crewai_platform_tools/test_constants.py diff --git a/lib/crewai-core/src/crewai_core/platform_apps.py b/lib/crewai-core/src/crewai_core/platform_apps.py new file mode 100644 index 000000000..5817fb412 --- /dev/null +++ b/lib/crewai-core/src/crewai_core/platform_apps.py @@ -0,0 +1,25 @@ +"""CrewAI Platform application catalog.""" + +from typing import Final, Literal, get_args + + +PlatformApp = Literal[ + "asana", + "box", + "clickup", + "github", + "gmail", + "google_calendar", + "google_sheets", + "hubspot", + "jira", + "linear", + "notion", + "salesforce", + "shopify", + "slack", + "stripe", + "zendesk", +] + +PLATFORM_APPS: Final[tuple[str, ...]] = (*get_args(PlatformApp),) diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/__init__.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/__init__.py index 4eb9d7d43..89bb29366 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/__init__.py +++ b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/__init__.py @@ -4,6 +4,8 @@ This module provides tools for integrating with various platform applications through the CrewAI platform API. """ +from crewai_core.platform_apps import PLATFORM_APPS + from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import ( CrewAIPlatformActionTool, ) @@ -13,6 +15,7 @@ from crewai_tools.tools.crewai_platform_tools.crewai_platform_tools import ( __all__ = [ + "PLATFORM_APPS", "CrewAIPlatformActionTool", "CrewaiPlatformTools", ] diff --git a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_constants.py b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_constants.py new file mode 100644 index 000000000..c1080223c --- /dev/null +++ b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_constants.py @@ -0,0 +1,22 @@ +from crewai_core.platform_apps import PLATFORM_APPS + + +def test_platform_apps_contains_supported_application_catalog() -> None: + assert PLATFORM_APPS == ( + "asana", + "box", + "clickup", + "github", + "gmail", + "google_calendar", + "google_sheets", + "hubspot", + "jira", + "linear", + "notion", + "salesforce", + "shopify", + "slack", + "stripe", + "zendesk", + ) diff --git a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py index 49fad6aac..a4b0e6675 100644 --- a/lib/crewai/src/crewai/agents/agent_builder/base_agent.py +++ b/lib/crewai/src/crewai/agents/agent_builder/base_agent.py @@ -9,6 +9,7 @@ import re from typing import TYPE_CHECKING, Annotated, Any, Final, Literal import uuid +from crewai_core.platform_apps import PlatformApp from pydantic import ( UUID4, BaseModel, @@ -180,25 +181,6 @@ _SLUG_RE: Final[re.Pattern[str]] = re.compile( ) -PlatformApp = Literal[ - "asana", - "box", - "clickup", - "github", - "gmail", - "google_calendar", - "google_sheets", - "hubspot", - "jira", - "linear", - "notion", - "salesforce", - "shopify", - "slack", - "stripe", - "zendesk", -] - PlatformAppOrAction = PlatformApp | str From d20845f0a3fc0db9ec2837abcf021780f295730a Mon Sep 17 00:00:00 2001 From: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:43:55 +0530 Subject: [PATCH 05/11] feat: add platform tools to JSON crew wizard (#7384) * feat: support platform tools in JSON crews * style: clarify platform integration labels * fix: avoid repeating tool picker title * fix(platform): surface JSON tool discovery errors --- lib/cli/src/crewai_cli/create_json_crew.py | 9 ++- .../src/crewai_cli/platform_tools_catalog.py | 29 +++++++++ lib/cli/src/crewai_cli/tui_picker.py | 5 +- lib/cli/tests/test_create_crew.py | 47 +++++++++++++++ .../crewai_platform_tools.py | 23 ++------ .../test_crewai_platform_tools.py | 6 +- lib/crewai/src/crewai/project/json_loader.py | 57 ++++++++++++++---- lib/crewai/tests/project/test_json_loader.py | 59 +++++++++++++++++++ 8 files changed, 199 insertions(+), 36 deletions(-) create mode 100644 lib/cli/src/crewai_cli/platform_tools_catalog.py diff --git a/lib/cli/src/crewai_cli/create_json_crew.py b/lib/cli/src/crewai_cli/create_json_crew.py index a4b04975f..00f1cd4db 100644 --- a/lib/cli/src/crewai_cli/create_json_crew.py +++ b/lib/cli/src/crewai_cli/create_json_crew.py @@ -16,6 +16,7 @@ from rich.text import Text from crewai_cli.constants import ENV_VARS from crewai_cli.git import initialize_if_git_available from crewai_cli.model_catalog import get_provider_models +from crewai_cli.platform_tools_catalog import PLATFORM_TOOLS from crewai_cli.tui_picker import pick_many, pick_one from crewai_cli.utils import ( enable_prompt_line_editing, @@ -103,6 +104,7 @@ _TEMPLATES_DIR = Path(__file__).parent / "templates" / "json_crew" # ── Common tools for picker ──────────────────────────────────── _TOOL_CATEGORIES: list[tuple[str, list[tuple[str, str]]]] = [ + ("CrewAI Platform", PLATFORM_TOOLS), ( "Search & Research", [ @@ -304,6 +306,9 @@ def _show_interpolation_hint(kind: str) -> None: def _tool_label(name: str, description: str) -> str: + if name.startswith("platform:"): + app_name = description.removesuffix(" Integration").replace(" ", "") + return f"{description:<48s} Platform: {app_name.replace(' ', '')}Integration" return f"{description:<48s} {name}" @@ -351,6 +356,7 @@ def _select_tools() -> list[str]: selected: set[str] = set() expanded: str | None = None focus_category: str | None = None + first_render = True while True: labels: list[str] = [] @@ -387,13 +393,14 @@ def _select_tools() -> list[str]: labels.append(_tool_label(name, desc)) indices, action = pick_many( - "Tools (space to toggle, enter to confirm):", + "Tools (space to toggle, enter to confirm):" if first_render else "", labels, action_indices=action_indices, separator_indices=separator_indices, preselected=preselected, initial_cursor=initial_cursor, ) + first_render = False # Carry over toggles made on this screen; tools not visible in this # render keep their previous state. diff --git a/lib/cli/src/crewai_cli/platform_tools_catalog.py b/lib/cli/src/crewai_cli/platform_tools_catalog.py new file mode 100644 index 000000000..6de79343f --- /dev/null +++ b/lib/cli/src/crewai_cli/platform_tools_catalog.py @@ -0,0 +1,29 @@ +"""CrewAI AMP platform tools exposed by the crew-creation wizard.""" + +from crewai_core.platform_apps import PLATFORM_APPS + + +PLATFORM_TOOL_PREFIX = "platform:" + +_APP_DESCRIPTIONS: dict[str, str] = { + "asana": "Asana Integration", + "box": "Box Integration", + "clickup": "ClickUp Integration", + "github": "GitHub Integration", + "gmail": "Gmail Integration", + "google_calendar": "Google Calendar Integration", + "google_sheets": "Google Sheets Integration", + "hubspot": "HubSpot Integration", + "jira": "Jira Integration", + "linear": "Linear Integration", + "notion": "Notion Integration", + "salesforce": "Salesforce Integration", + "shopify": "Shopify Integration", + "slack": "Slack Integration", + "stripe": "Stripe Integration", + "zendesk": "Zendesk Integration", +} + +PLATFORM_TOOLS: list[tuple[str, str]] = [ + (f"{PLATFORM_TOOL_PREFIX}{app}", _APP_DESCRIPTIONS[app]) for app in PLATFORM_APPS +] diff --git a/lib/cli/src/crewai_cli/tui_picker.py b/lib/cli/src/crewai_cli/tui_picker.py index 69157658c..41b630620 100644 --- a/lib/cli/src/crewai_cli/tui_picker.py +++ b/lib/cli/src/crewai_cli/tui_picker.py @@ -388,8 +388,9 @@ def pick_many( Sorted list of selected indices, or ``(indices, action_index)`` when ``action_indices`` is provided. """ - click.echo() - click.secho(f" {title}", fg="cyan") + if title: + click.echo() + click.secho(f" {title}", fg="cyan") if _is_interactive(): try: diff --git a/lib/cli/tests/test_create_crew.py b/lib/cli/tests/test_create_crew.py index cb2a4820b..b2937dc33 100644 --- a/lib/cli/tests/test_create_crew.py +++ b/lib/cli/tests/test_create_crew.py @@ -7,6 +7,7 @@ from unittest import mock import pytest import tomli from click.testing import CliRunner +from crewai_core.platform_apps import PLATFORM_APPS from packaging.requirements import Requirement from packaging.version import Version import crewai_cli.create_json_crew as json_crew @@ -620,6 +621,52 @@ def test_json_wizard_tool_picker_lists_builtin_tools_across_categories(monkeypat }.isdisjoint(tool_names) +def test_json_wizard_platform_tool_selection_stays_in_agent_tools(monkeypatch): + picker_calls = 0 + + def pick_many(title: str, labels: list[str], **kwargs): + nonlocal picker_calls + picker_calls += 1 + if picker_calls == 1: + platform_row = next( + idx for idx, label in enumerate(labels) if "CrewAI Platform" in label + ) + return [], platform_row + + github = next( + idx + for idx, label in enumerate(labels) + if label.startswith("GitHub Integration") + and label.endswith("Platform: GitHubIntegration") + ) + return [github], None + + monkeypatch.setattr(json_crew, "pick_many", pick_many) + monkeypatch.setattr( + json_crew, "_prompt_text", lambda label, **kwargs: label.lower() + ) + monkeypatch.setattr(json_crew, "_select_model", lambda: "openai/gpt-5.5") + monkeypatch.setattr(json_crew, "_confirm", lambda *_args, **_kwargs: False) + + agent = json_crew._wizard_agent(agent_num=1, existing_names=[]) + + assert agent is not None + assert agent["tools"] == ["platform:github"] + assert '"tools": ["platform:github"]' in json_crew._agent_to_jsonc(agent) + + +def test_json_wizard_platform_catalog_contains_every_supported_app(): + platform_category = next( + tools + for category, tools in json_crew._TOOL_CATEGORIES + if category == "CrewAI Platform" + ) + + assert [name for name, _description in platform_category] == [ + f"platform:{app}" for app in PLATFORM_APPS + ] + + def test_multi_picker_skips_separator_on_initial_cursor(monkeypatch): cursors: list[int] = [] diff --git a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py index 2dc083b98..a344f8cae 100644 --- a/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py +++ b/lib/crewai-tools/src/crewai_tools/tools/crewai_platform_tools/crewai_platform_tools.py @@ -1,5 +1,3 @@ -import logging - from crewai.tools import BaseTool from crewai_tools.tools.crewai_platform_tools.crewai_platform_action_tool import ( @@ -11,9 +9,6 @@ from crewai_tools.tools.crewai_platform_tools.integrations_client import ( ) -logger = logging.getLogger(__name__) - - def CrewaiPlatformTools( # noqa: N802 apps: list[str], ) -> list[BaseTool]: @@ -28,17 +23,11 @@ def CrewaiPlatformTools( # noqa: N802 selectors = [ApplicationSelector.from_string(app) for app in apps] tools: list[BaseTool] = [] - try: - for selector in selectors: - client = client_for_selector(selector) - tools.extend( - CrewAIPlatformActionTool(tool_info, client=client) - for tool_info in client.get_actions([selector]) - ) - except ValueError: - raise - except Exception as error: - logger.error(f"Failed to fetch platform tools for apps {apps}: {error}") - return [] + for selector in selectors: + client = client_for_selector(selector) + tools.extend( + CrewAIPlatformActionTool(tool_info, client=client) + for tool_info in client.get_actions([selector]) + ) return tools diff --git a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py index 62b0814fc..f3013675f 100644 --- a/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py +++ b/lib/crewai-tools/tests/tools/crewai_platform_tools/test_crewai_platform_tools.py @@ -157,10 +157,8 @@ class TestCrewaiPlatformTools(unittest.TestCase): def test_crewai_platform_tools_api_error_handling(self, mock_get): mock_get.side_effect = Exception("API Error") - tools = CrewaiPlatformTools(apps=["github"]) - assert tools is not None - assert isinstance(tools, list) - assert len(tools) == 0 + with self.assertRaisesRegex(Exception, "API Error"): + CrewaiPlatformTools(apps=["github"]) def test_crewai_platform_tools_no_token(self): with patch.dict("os.environ", {}, clear=True): diff --git a/lib/crewai/src/crewai/project/json_loader.py b/lib/crewai/src/crewai/project/json_loader.py index c28d42cc3..c1c95a83a 100644 --- a/lib/crewai/src/crewai/project/json_loader.py +++ b/lib/crewai/src/crewai/project/json_loader.py @@ -1820,6 +1820,9 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li ) if not tool_def: continue + if tool_def.startswith("platform:"): + tools.extend(_resolve_platform_tools(tool_def.removeprefix("platform:"))) + continue if tool_def.startswith("custom:"): tools.append(_resolve_custom_tool(tool_def[7:], project_root=project_root)) continue @@ -1847,6 +1850,30 @@ def _resolve_tools(tool_defs: list[Any], project_root: Path | None = None) -> li return tools +def _resolve_platform_tools(selector: str) -> list[Any]: + """Materialize an AMP application selector into CrewAI tools.""" + if not selector: + raise JSONProjectError( + "Invalid platform tool reference 'platform:': expected " + "'platform:'" + ) + + try: + from crewai_tools import CrewaiPlatformTools + except ImportError as e: + raise JSONProjectError( + "Platform tools require the 'crewai-tools' package. " + "Install CrewAI with the tools extra." + ) from e + + try: + return CrewaiPlatformTools(apps=[selector]) + except Exception as e: + raise JSONProjectError( + f"Failed to initialize platform tool 'platform:{selector}': {e}" + ) from e + + def _instantiate_tool_import_ref(ref: str) -> Any: from crewai.tools import BaseTool @@ -1990,19 +2017,25 @@ def _tool_definition_errors( f"got {type(tool_def).__name__}" ) continue - if not tool_def.startswith("custom:"): + if tool_def.startswith("platform:"): + if not tool_def.removeprefix("platform:"): + errors.append( + f"{source}: invalid platform tool reference 'platform:': " + "expected 'platform:'" + ) continue - try: - tool_file = _custom_tool_file(tool_def[7:], project_root) - except JSONProjectError as exc: - errors.append(f"{source}: {exc}") - continue - if not tool_file.exists(): - errors.append( - f"{source}: custom tool '{tool_def}' not found: expected " - f"{tool_file}. Create the file with a BaseTool subclass, or " - f"remove the tool from your crew JSON." - ) + if tool_def.startswith("custom:"): + try: + tool_file = _custom_tool_file(tool_def[7:], project_root) + except JSONProjectError as exc: + errors.append(f"{source}: {exc}") + continue + if not tool_file.exists(): + errors.append( + f"{source}: custom tool '{tool_def}' not found: expected " + f"{tool_file}. Create the file with a BaseTool subclass, or " + f"remove the tool from your crew JSON." + ) return errors diff --git a/lib/crewai/tests/project/test_json_loader.py b/lib/crewai/tests/project/test_json_loader.py index 638c0d500..727aebbb9 100644 --- a/lib/crewai/tests/project/test_json_loader.py +++ b/lib/crewai/tests/project/test_json_loader.py @@ -403,6 +403,52 @@ class TestLoadAgentFromDefinition: class TestResolveTools: + def test_platform_tool_refs_materialize_multiple_application_tools( + self, monkeypatch + ): + from crewai.project.json_loader import _resolve_tools + + github_tools = [object()] + linear_tools = [object(), object()] + calls: list[list[str]] = [] + + def build_platform_tools(apps: list[str]): + calls.append(apps) + return { + "github": github_tools, + "linear": linear_tools, + }[apps[0]] + + monkeypatch.setattr( + "crewai_tools.CrewaiPlatformTools", build_platform_tools + ) + + tools = _resolve_tools(["platform:github", "platform:linear"]) + + assert tools == github_tools + linear_tools + assert calls == [["github"], ["linear"]] + + def test_empty_platform_tool_ref_raises_with_guidance(self): + from crewai.project.json_loader import JSONProjectError, _resolve_tools + + with pytest.raises(JSONProjectError, match="platform:"): + _resolve_tools(["platform:"]) + + def test_platform_tool_discovery_errors_are_json_project_errors( + self, monkeypatch + ): + from crewai.project.json_loader import JSONProjectError, _resolve_tools + + def fail_platform_tool_discovery(apps: list[str]): + raise RuntimeError("discovery unavailable") + + monkeypatch.setattr( + "crewai_tools.CrewaiPlatformTools", fail_platform_tool_discovery + ) + + with pytest.raises(JSONProjectError, match="discovery unavailable"): + _resolve_tools(["platform:github"]) + def test_import_ref_tool_resolves(self, tmp_path, monkeypatch): from crewai.project.json_loader import _resolve_tools @@ -614,6 +660,19 @@ class TestValidationDoesNotExecuteTools: assert "Invalid custom tool name" in str(exc_info.value) + def test_validate_rejects_empty_platform_tool_ref(self, tmp_path): + from crewai.project.json_loader import ( + JSONProjectValidationError, + validate_crew_project, + ) + + crew_path = self._write_project(tmp_path, tool_line='"platform:"') + + with pytest.raises(JSONProjectValidationError) as exc_info: + validate_crew_project(crew_path, tmp_path / "agents") + + assert "platform:" in str(exc_info.value) + def test_validate_rejects_deep_python_ref_nesting(self, tmp_path): from crewai.project.json_loader import validate_crew_project From 93ad4d67c47c634d0b413a707d8bdc2b2cdfd62f Mon Sep 17 00:00:00 2001 From: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:50:33 +0530 Subject: [PATCH 06/11] feat: validate platform integrations during crew setup (#7385) * feat: validate platform integrations during crew setup * fix: clarify platform integration validation * refactor: split platform authentication workflow * feat: list connected platform integrations --- lib/cli/src/crewai_cli/create_json_crew.py | 194 ++++++++++++++++++++- 1 file changed, 193 insertions(+), 1 deletion(-) diff --git a/lib/cli/src/crewai_cli/create_json_crew.py b/lib/cli/src/crewai_cli/create_json_crew.py index 00f1cd4db..362f98e16 100644 --- a/lib/cli/src/crewai_cli/create_json_crew.py +++ b/lib/cli/src/crewai_cli/create_json_crew.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +import os from pathlib import Path import re import sys @@ -619,6 +620,10 @@ def _wizard_agents_and_tasks( "inputs": {}, } + # Platform authentication belongs to the final wizard step, after the + # user has finished configuring agents, tasks, and crew settings. + _setup_platform_auth(agents) + return agents, tasks, crew_settings @@ -871,6 +876,181 @@ def _setup_env(folder_path: Path, llm_model: str) -> None: click.secho(" API keys and model saved to .env file", fg="green") +def _platform_apps_from_agents(agents: list[dict[str, Any]]) -> list[str]: + """Return unique platform applications selected across all agents.""" + apps: list[str] = [] + for agent in agents: + for tool in agent.get("tools", []): + if isinstance(tool, str) and tool.startswith("platform:"): + app = tool.removeprefix("platform:") + if app and app not in apps: + apps.append(app) + return apps + + +def _platform_app_name(app: str) -> str: + """Return the display name for a platform application slug.""" + return ( + dict(PLATFORM_TOOLS) + .get(f"platform:{app}", app.replace("_", " ").title()) + .removesuffix(" Integration") + ) + + +def _prompt_platform_token() -> str: + """Explain how to obtain and securely prompt for an AMP integration token.""" + click.secho( + " To use CrewAI Platform tools, you need a CrewAI Platform Integration Token.", + fg="yellow", + ) + click.secho( + " Get your token from CrewAI AMP: https://app.crewai.com " + "→ Settings → Integration Tokens.", + fg="cyan", + ) + return str( + click.prompt( + click.style(" CREWAI_PLATFORM_INTEGRATION_TOKEN", fg="cyan"), + hide_input=True, + prompt_suffix=click.style(" > ", fg="bright_white"), + ) + ).strip() + + +def _validate_platform_apps( + apps: list[str], application_selector: Any, client_for_selector: Any +) -> tuple[list[str], bool]: + """Check selected AMP applications and return failures and token validity.""" + failed: list[str] = [] + for app in apps: + app_name = _platform_app_name(app) + click.echo() + click.secho( + " Checking CrewAI Platform Integration Token and " + f"{app_name} integration on AMP...", + fg="cyan", + ) + try: + selector = application_selector.from_string(app) + actions = client_for_selector(selector).get_actions([selector]) + except Exception as error: + status_code = getattr(getattr(error, "response", None), "status_code", None) + if status_code in {401, 403}: + click.secho( + " ✘ CrewAI Platform Integration Token is invalid or expired", + fg="red", + ) + return failed, True + click.secho( + f" ✘ {app_name} integration could not be validated: {error}", + fg="red", + ) + failed.append(app) + continue + + if not actions: + click.secho( + f" ✘ {app_name} integration is not connected on CrewAI Platform", + fg="red", + ) + failed.append(app) + else: + click.secho( + f" ✔ {app_name} integration is connected on CrewAI Platform", + fg="green", + ) + return failed, False + + +def _show_platform_validation_guidance( + failed_apps: list[str], token_invalid: bool +) -> None: + """Tell the user what to fix before revalidating AMP integrations.""" + click.echo() + if token_invalid: + click.secho( + " Check your CrewAI Platform Integration Token in AMP.", + fg="yellow", + ) + return + + failed_app_names = [_platform_app_name(app) for app in failed_apps] + click.secho( + " Check the " + f"{', '.join(failed_app_names)} integration" + f"{'s' if len(failed_app_names) != 1 else ''} and your CrewAI " + "Platform Integration Token in AMP.", + fg="yellow", + ) + + +def _prompt_platform_revalidation_token() -> str: + """Prompt for an optional replacement token before the next validation pass.""" + click.echo() + return str( + click.prompt( + click.style( + " Press Enter to revalidate, or enter a replacement token", + fg="cyan", + ), + default="", + show_default=False, + hide_input=True, + prompt_suffix=click.style(" > ", fg="bright_white"), + ) + ).strip() + + +def _setup_platform_auth(agents: list[dict[str, Any]]) -> str | None: + """Get and validate AMP authentication for selected platform applications.""" + apps = _platform_apps_from_agents(agents) + if not apps: + return None + + click.echo() + try: + from crewai_tools.tools.crewai_platform_tools.integrations_client import ( + ApplicationSelector, + client_for_selector, + ) + except ImportError as error: + raise click.ClickException( + "Platform tools require the 'crewai-tools' package. " + "Install it with `uv add crewai-tools` or " + "`pip install 'crewai[tools]'`." + ) from error + + token = os.environ.get("CREWAI_PLATFORM_INTEGRATION_TOKEN", "") + while True: + if not token: + token = _prompt_platform_token() + if not token: + click.secho( + " A CrewAI Platform Integration Token is required to validate " + "the selected integrations.", + fg="yellow", + ) + continue + + os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = token + failed_apps, token_invalid = _validate_platform_apps( + apps, ApplicationSelector, client_for_selector + ) + if not failed_apps and not token_invalid: + _success("CrewAI Platform integration token set", bold=True) + _success( + "CrewAI Platform integrations connected: " + f"{', '.join(_platform_app_name(app) for app in apps)}" + ) + return token + + _show_platform_validation_guidance(failed_apps, token_invalid) + replacement_token = _prompt_platform_revalidation_token() + if replacement_token: + token = replacement_token + os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = token + + # ── Main ──────────────────────────────────────────────────────── @@ -923,13 +1103,25 @@ def create_json_crew( default_llm=default_llm, ) - # Create directories + platform_token = ( + os.environ.get("CREWAI_PLATFORM_INTEGRATION_TOKEN") + if _platform_apps_from_agents(agents) and not dmn_mode + else None + ) + + # Create directories only after platform authentication succeeds. folder_path.mkdir(parents=True) (folder_path / "agents").mkdir() (folder_path / "tools").mkdir() (folder_path / "skills").mkdir() (folder_path / "knowledge").mkdir() + if platform_token: + os.environ["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = platform_token + env_vars = load_env_vars(folder_path) + env_vars["CREWAI_PLATFORM_INTEGRATION_TOKEN"] = platform_token + write_env_file(folder_path, env_vars) + for agent in agents: _write_jsonc( folder_path / "agents" / f"{agent['name']}.jsonc", From 894898f84c4ac0a89f24bf7bee6c381eb0e67f51 Mon Sep 17 00:00:00 2001 From: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> Date: Sat, 12 Sep 2026 01:16:17 +0530 Subject: [PATCH 07/11] ci: welcome first-time contributors after merge (#7397) * ci: welcome first-time contributors after merge * ci: welcome first-time contributors after merge * ci: support fork contributor welcome comments * ci: minimize contributor welcome permissions --- .../workflows/first-contributor-welcome.yml | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 .github/workflows/first-contributor-welcome.yml diff --git a/.github/workflows/first-contributor-welcome.yml b/.github/workflows/first-contributor-welcome.yml new file mode 100644 index 000000000..69c153db8 --- /dev/null +++ b/.github/workflows/first-contributor-welcome.yml @@ -0,0 +1,88 @@ +name: Welcome First-Time Contributors + +on: + pull_request_target: + types: [closed] + +permissions: + contents: read + issues: write + +jobs: + welcome: + # Match the first-time contributor definition used by ftc-require-issue. + if: > + github.event.pull_request.merged == true && + github.event.pull_request.user.type != 'Bot' && + !contains(fromJSON('["MEMBER","OWNER","COLLABORATOR","CONTRIBUTOR"]'), + github.event.pull_request.author_association) + runs-on: ubuntu-latest + steps: + - name: Thank first-time contributors + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const pullRequest = context.payload.pull_request; + const owner = context.repo.owner; + const repo = context.repo.repo; + const marker = ""; + + const existingComments = await github.paginate( + github.rest.issues.listComments, + { + owner, + repo, + issue_number: pullRequest.number, + per_page: 100, + }, + ); + if (existingComments.some((comment) => comment.body?.includes(marker))) { + core.info("Welcome comment already exists; skipping duplicate."); + return; + } + + const prUrl = pullRequest.html_url; + const shareText = [ + "I just made my first contribution to @crewAIInc!", + "", + "Excited to help build the future of AI agents with CrewAI.", + "", + prUrl, + "", + "#OpenSource #AI #CrewAI", + ].join("\n"); + const xShareUrl = `https://x.com/intent/post?text=${encodeURIComponent(shareText)}`; + const linkedInShareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(prUrl)}`; + + const body = [ + marker, + "", + `## Thanks for your first contribution to CrewAI, @${pullRequest.user.login}!`, + "", + "We really appreciate the time and care you put into this PR. Your work is now part of CrewAI — welcome to the contributor community.", + "", + "Want to share your contribution? Totally optional:", + "", + `[Share on X](${xShareUrl}) · [Share the PR on LinkedIn](${linkedInShareUrl})`, + "", + "If the links don't work, feel free to copy and personalize this:", + "", + "```text", + "I just made my first contribution to @crewAIInc!", + "", + "Excited to help build the future of AI agents with CrewAI.", + "", + prUrl, + "", + "#OpenSource #AI #CrewAI", + "```", + "", + "If you share, we'd love to see it — tag **@crewAIInc** on X and **CrewAI** on LinkedIn.", + ].join("\n"); + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pullRequest.number, + body, + }); From 21678f8ac6254dfb1b4c9b3ab37882263bb2219e Mon Sep 17 00:00:00 2001 From: Melanie Hart Buehler Date: Sun, 13 Sep 2026 22:51:43 -0700 Subject: [PATCH 08/11] docs(rag): add xpu to embedding device options (#6808) * docs(rag): add xpu to embedding device options * address copilot review * docs(rag): sync Korean and Portuguese RagTool pages --------- Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> --- docs/edge/ar/tools/ai-ml/ragtool.mdx | 4 +- docs/edge/en/tools/ai-ml/ragtool.mdx | 4 +- docs/edge/ko/tools/ai-ml/ragtool.mdx | 567 ++++++++++++++++-- docs/edge/pt-BR/tools/ai-ml/ragtool.mdx | 555 +++++++++++++++-- .../instructor/instructor_provider.py | 2 +- .../sentence_transformer_provider.py | 2 +- .../embeddings/test_backward_compatibility.py | 14 +- 7 files changed, 1058 insertions(+), 90 deletions(-) diff --git a/docs/edge/ar/tools/ai-ml/ragtool.mdx b/docs/edge/ar/tools/ai-ml/ragtool.mdx index a73709b69..a92d50c82 100644 --- a/docs/edge/ar/tools/ai-ml/ragtool.mdx +++ b/docs/edge/ar/tools/ai-ml/ragtool.mdx @@ -460,7 +460,7 @@ rag_tool = RagTool(config=config, summarize=True) **خيارات الإعداد:** - `model_name` (str): معرّف نموذج HuggingFace. القيمة الافتراضية: `hkunlp/instructor-base`. الخيارات: `hkunlp/instructor-xl`، `hkunlp/instructor-large`، `hkunlp/instructor-base` - - `device` (str): الجهاز للتشغيل. القيمة الافتراضية: `cpu`. الخيارات: `cpu`، `cuda`، `mps` + - `device` (str): الجهاز للتشغيل. القيمة الافتراضية: `cpu`. الخيارات: `cpu`، `cuda`، `mps`، `xpu` - `instruction` (str): بادئة التعليمات للتضمينات **متغيرات البيئة:** @@ -485,7 +485,7 @@ rag_tool = RagTool(config=config, summarize=True) **خيارات الإعداد:** - `model_name` (str): اسم نموذج Sentence Transformers. القيمة الافتراضية: `all-MiniLM-L6-v2`. الخيارات: `all-mpnet-base-v2`، `all-MiniLM-L6-v2`، `paraphrase-multilingual-MiniLM-L12-v2` - - `device` (str): الجهاز للتشغيل. القيمة الافتراضية: `cpu`. الخيارات: `cpu`، `cuda`، `mps` + - `device` (str): الجهاز للتشغيل. القيمة الافتراضية: `cpu`. الخيارات: `cpu`، `cuda`، `mps`، `xpu` - `normalize_embeddings` (bool): ما إذا كان يتم تطبيع التضمينات. القيمة الافتراضية: `False` **متغيرات البيئة:** diff --git a/docs/edge/en/tools/ai-ml/ragtool.mdx b/docs/edge/en/tools/ai-ml/ragtool.mdx index 0380c4bac..b9d66769f 100644 --- a/docs/edge/en/tools/ai-ml/ragtool.mdx +++ b/docs/edge/en/tools/ai-ml/ragtool.mdx @@ -460,7 +460,7 @@ The `embedding_model` parameter accepts a `crewai.rag.embeddings.types.ProviderS **Config Options:** - `model_name` (str): HuggingFace model ID. Default: `hkunlp/instructor-base`. Options: `hkunlp/instructor-xl`, `hkunlp/instructor-large`, `hkunlp/instructor-base` - - `device` (str): Device to run on. Default: `cpu`. Options: `cpu`, `cuda`, `mps` + - `device` (str): Device to run on. Default: `cpu`. Options: `cpu`, `cuda`, `mps`, `xpu` - `instruction` (str): Instruction prefix for embeddings **Environment Variables:** @@ -485,7 +485,7 @@ The `embedding_model` parameter accepts a `crewai.rag.embeddings.types.ProviderS **Config Options:** - `model_name` (str): Sentence Transformers model name. Default: `all-MiniLM-L6-v2`. Options: `all-mpnet-base-v2`, `all-MiniLM-L6-v2`, `paraphrase-multilingual-MiniLM-L12-v2` - - `device` (str): Device to run on. Default: `cpu`. Options: `cpu`, `cuda`, `mps` + - `device` (str): Device to run on. Default: `cpu`. Options: `cpu`, `cuda`, `mps`, `xpu` - `normalize_embeddings` (bool): Whether to normalize embeddings. Default: `False` **Environment Variables:** diff --git a/docs/edge/ko/tools/ai-ml/ragtool.mdx b/docs/edge/ko/tools/ai-ml/ragtool.mdx index 241e579f4..5bcadccd6 100644 --- a/docs/edge/ko/tools/ai-ml/ragtool.mdx +++ b/docs/edge/ko/tools/ai-ml/ragtool.mdx @@ -1,6 +1,6 @@ --- title: RAG 도구 -description: RagTool은 Retrieval-Augmented Generation을 사용하여 질문에 답변하는 동적 지식 기반 도구입니다. +description: `RagTool`은 Retrieval-Augmented Generation을 사용하여 질문에 답변하는 동적 지식 기반 도구입니다. icon: vector-square mode: "wide" --- @@ -9,7 +9,7 @@ mode: "wide" ## 설명 -`RagTool`은 EmbedChain을 통한 RAG(Retrieval-Augmented Generation)의 강력함을 활용하여 질문에 답하도록 설계되었습니다. +`RagTool`은 CrewAI의 네이티브 RAG 시스템을 통해 RAG(Retrieval-Augmented Generation)의 강력함을 활용하여 질문에 답하도록 설계되었습니다. 이는 다양한 데이터 소스에서 관련 정보를 검색할 수 있는 동적 지식 기반을 제공합니다. 이 도구는 방대한 정보에 접근해야 하고 맥락에 맞는 답변을 제공해야 하는 애플리케이션에 특히 유용합니다. @@ -76,24 +76,24 @@ def knowledge_expert(self) -> Agent: `RagTool`은 다음과 같은 매개변수를 허용합니다: - **summarize**: 선택 사항. 검색된 콘텐츠를 요약할지 여부입니다. 기본값은 `False`입니다. -- **adapter**: 선택 사항. 지식 베이스에 대한 사용자 지정 어댑터입니다. 제공되지 않은 경우 EmbedchainAdapter가 사용됩니다. -- **config**: 선택 사항. 내부 EmbedChain App의 구성입니다. +- **adapter**: 선택 사항. 지식 기반을 위한 사용자 지정 어댑터입니다. 제공하지 않으면 CrewAIRagAdapter가 사용됩니다. +- **config**: 선택 사항. 내부 CrewAI RAG 시스템에 대한 구성입니다. 선택적 `embedding_model`(ProviderSpec) 및 `vectordb`(VectorDbConfig) 키를 포함하는 `RagToolConfig` TypedDict를 허용합니다. 프로그래밍 방식으로 제공된 모든 구성 값은 환경 변수보다 우선합니다. ## 콘텐츠 추가 `add` 메서드를 사용하여 지식 베이스에 콘텐츠를 추가할 수 있습니다: ```python Code -# PDF 파일 추가 +# Add a PDF file rag_tool.add(data_type="file", path="path/to/your/document.pdf") -# 웹 페이지 추가 +# Add a web page rag_tool.add(data_type="web_page", url="https://example.com") -# YouTube 비디오 추가 +# Add a YouTube video rag_tool.add(data_type="youtube_video", url="https://www.youtube.com/watch?v=VIDEO_ID") -# 파일이 있는 디렉터리 추가 +# Add a directory of files rag_tool.add(data_type="directory", path="path/to/your/directory") ``` @@ -123,51 +123,532 @@ def knowledge_expert(self) -> Agent: ## 고급 구성 -`RagTool`의 동작을 구성 사전을 제공하여 사용자 지정할 수 있습니다. +구성 딕셔너리를 제공하여 `RagTool`의 동작을 사용자 지정할 수 있습니다. ```python Code from crewai_tools import RagTool +from crewai_tools.tools.rag import RagToolConfig, VectorDbConfig, ProviderSpec -# 사용자 지정 구성으로 RAG 도구 생성 -config = { - "app": { - "name": "custom_app", - }, - "llm": { - "provider": "openai", - "config": { - "model": "gpt-4", - } - }, - "embedding_model": { - "provider": "openai", - "config": { - "model": "text-embedding-ada-002" - } - }, - "vectordb": { - "provider": "elasticsearch", - "config": { - "collection_name": "my-collection", - "cloud_id": "deployment-name:xxxx", - "api_key": "your-key", - "verify_certs": False - } - }, - "chunker": { - "chunk_size": 400, - "chunk_overlap": 100, - "length_function": "len", - "min_chunk_size": 0 +# Create a RAG tool with custom configuration + +vectordb: VectorDbConfig = { + "provider": "qdrant", + "config": { + "collection_name": "my-collection" } } +embedding_model: ProviderSpec = { + "provider": "openai", + "config": { + "model_name": "text-embedding-3-small" + } +} + +config: RagToolConfig = { + "vectordb": vectordb, + "embedding_model": embedding_model +} + rag_tool = RagTool(config=config, summarize=True) ``` -내부 RAG 도구는 Embedchain 어댑터를 사용하므로 Embedchain에서 지원하는 모든 구성 옵션을 전달할 수 있습니다. -자세한 내용은 [Embedchain 문서](https://docs.embedchain.ai/components/introduction)를 참조하세요. -.yaml 파일에서 제공되는 구성 옵션을 반드시 검토하시기 바랍니다. +## 임베딩 모델 구성 + +`embedding_model` 매개변수는 다음 구조의 `crewai.rag.embeddings.types.ProviderSpec` 딕셔너리를 허용합니다. + +```python +{ + "provider": "provider-name", # Required + "config": { # Optional + # Provider-specific configuration + } +} +``` + +### 지원되는 프로바이더 + + + + ```python main.py + from crewai.rag.embeddings.providers.openai.types import OpenAIProviderSpec + + embedding_model: OpenAIProviderSpec = { + "provider": "openai", + "config": { + "api_key": "your-api-key", + "model_name": "text-embedding-ada-002", + "dimensions": 1536, + "organization_id": "your-org-id", + "api_base": "https://api.openai.com/v1", + "api_version": "v1", + "default_headers": {"Custom-Header": "value"} + } + } + ``` + + **구성 옵션:** + - `api_key` (str): OpenAI API 키 + - `model_name` (str): 사용할 모델. 기본값: `text-embedding-ada-002`. 옵션: `text-embedding-3-small`, `text-embedding-3-large`, `text-embedding-ada-002` + - `dimensions` (int): 임베딩 차원 수 + - `organization_id` (str): OpenAI 조직 ID + - `api_base` (str): 사용자 지정 API 기본 URL + - `api_version` (str): API 버전 + - `default_headers` (dict): API 요청을 위한 사용자 지정 헤더 + + **환경 변수:** + - `OPENAI_API_KEY` 또는 `EMBEDDINGS_OPENAI_API_KEY`: `api_key` + - `OPENAI_ORGANIZATION_ID` 또는 `EMBEDDINGS_OPENAI_ORGANIZATION_ID`: `organization_id` + - `OPENAI_MODEL_NAME` 또는 `EMBEDDINGS_OPENAI_MODEL_NAME`: `model_name` + - `OPENAI_API_BASE` 또는 `EMBEDDINGS_OPENAI_API_BASE`: `api_base` + - `OPENAI_API_VERSION` 또는 `EMBEDDINGS_OPENAI_API_VERSION`: `api_version` + - `OPENAI_DIMENSIONS` 또는 `EMBEDDINGS_OPENAI_DIMENSIONS`: `dimensions` + + + + ```python main.py + from crewai.rag.embeddings.providers.cohere.types import CohereProviderSpec + + embedding_model: CohereProviderSpec = { + "provider": "cohere", + "config": { + "api_key": "your-api-key", + "model_name": "embed-english-v3.0" + } + } + ``` + + **구성 옵션:** + - `api_key` (str): Cohere API 키 + - `model_name` (str): 사용할 모델. 기본값: `large`. 옵션: `embed-english-v3.0`, `embed-multilingual-v3.0`, `large`, `small` + + **환경 변수:** + - `COHERE_API_KEY` 또는 `EMBEDDINGS_COHERE_API_KEY`: `api_key` + - `EMBEDDINGS_COHERE_MODEL_NAME`: `model_name` + + + + ```python main.py + from crewai.rag.embeddings.providers.voyageai.types import VoyageAIProviderSpec + + embedding_model: VoyageAIProviderSpec = { + "provider": "voyageai", + "config": { + "api_key": "your-api-key", + "model": "voyage-3", + "input_type": "document", + "truncation": True, + "output_dtype": "float32", + "output_dimension": 1024, + "max_retries": 3, + "timeout": 60.0 + } + } + ``` + + **구성 옵션:** + - `api_key` (str): VoyageAI API 키 + - `model` (str): 사용할 모델. 기본값: `voyage-2`. 옵션: `voyage-3`, `voyage-3-lite`, `voyage-code-3`, `voyage-large-2` + - `input_type` (str): 입력 유형. 옵션: `document`(저장용), `query`(검색용) + - `truncation` (bool): 최대 길이를 초과하는 입력을 잘라낼지 여부. 기본값: `True` + - `output_dtype` (str): 출력 데이터 유형 + - `output_dimension` (int): 출력 임베딩 차원 + - `max_retries` (int): 최대 재시도 횟수. 기본값: `0` + - `timeout` (float): 요청 시간 제한(초) + + **환경 변수:** + - `VOYAGEAI_API_KEY` 또는 `EMBEDDINGS_VOYAGEAI_API_KEY`: `api_key` + - `VOYAGEAI_MODEL` 또는 `EMBEDDINGS_VOYAGEAI_MODEL`: `model` + - `VOYAGEAI_INPUT_TYPE` 또는 `EMBEDDINGS_VOYAGEAI_INPUT_TYPE`: `input_type` + - `VOYAGEAI_TRUNCATION` 또는 `EMBEDDINGS_VOYAGEAI_TRUNCATION`: `truncation` + - `VOYAGEAI_OUTPUT_DTYPE` 또는 `EMBEDDINGS_VOYAGEAI_OUTPUT_DTYPE`: `output_dtype` + - `VOYAGEAI_OUTPUT_DIMENSION` 또는 `EMBEDDINGS_VOYAGEAI_OUTPUT_DIMENSION`: `output_dimension` + - `VOYAGEAI_MAX_RETRIES` 또는 `EMBEDDINGS_VOYAGEAI_MAX_RETRIES`: `max_retries` + - `VOYAGEAI_TIMEOUT` 또는 `EMBEDDINGS_VOYAGEAI_TIMEOUT`: `timeout` + + + + ```python main.py + from crewai.rag.embeddings.providers.ollama.types import OllamaProviderSpec + + embedding_model: OllamaProviderSpec = { + "provider": "ollama", + "config": { + "model_name": "llama2", + "url": "http://localhost:11434/api/embeddings" + } + } + ``` + + **구성 옵션:** + - `model_name` (str): Ollama 모델 이름(예: `llama2`, `mistral`, `nomic-embed-text`) + - `url` (str): Ollama API 엔드포인트 URL. 기본값: `http://localhost:11434/api/embeddings` + + **환경 변수:** + - `OLLAMA_MODEL` 또는 `EMBEDDINGS_OLLAMA_MODEL`: `model_name` + - `OLLAMA_URL` 또는 `EMBEDDINGS_OLLAMA_URL`: `url` + + + + ```python main.py + from crewai.rag.embeddings.providers.aws.types import BedrockProviderSpec + + embedding_model: BedrockProviderSpec = { + "provider": "amazon-bedrock", + "config": { + "model_name": "amazon.titan-embed-text-v2:0", + "session": boto3_session + } + } + ``` + + **구성 옵션:** + - `model_name` (str): Bedrock 모델 ID. 기본값: `amazon.titan-embed-text-v1`. 옵션: `amazon.titan-embed-text-v1`, `amazon.titan-embed-text-v2:0`, `cohere.embed-english-v3`, `cohere.embed-multilingual-v3` + - `session` (Any): AWS 인증을 위한 Boto3 세션 객체 + + **환경 변수:** + - `AWS_ACCESS_KEY_ID`: AWS 액세스 키 + - `AWS_SECRET_ACCESS_KEY`: AWS 비밀 키 + - `AWS_REGION`: AWS 리전(예: `us-east-1`) + + + + ```python main.py + from crewai.rag.embeddings.providers.microsoft.types import AzureProviderSpec + + embedding_model: AzureProviderSpec = { + "provider": "azure", + "config": { + "deployment_id": "your-deployment-id", + "api_key": "your-api-key", + "api_base": "https://your-resource.openai.azure.com", + "api_version": "2024-02-01", + "model_name": "text-embedding-ada-002", + "api_type": "azure" + } + } + ``` + + **구성 옵션:** + - `deployment_id` (str): **필수** - Azure OpenAI 배포 ID + - `api_key` (str): Azure OpenAI API 키 + - `api_base` (str): Azure OpenAI 리소스 엔드포인트 + - `api_version` (str): API 버전. 예: `2024-02-01` + - `model_name` (str): 모델 이름. 기본값: `text-embedding-ada-002` + - `api_type` (str): API 유형. 기본값: `azure` + - `dimensions` (int): 출력 차원 + - `default_headers` (dict): 사용자 지정 헤더 + + **환경 변수:** + - `AZURE_OPENAI_API_KEY` 또는 `EMBEDDINGS_AZURE_API_KEY`: `api_key` + - `AZURE_OPENAI_ENDPOINT` 또는 `EMBEDDINGS_AZURE_API_BASE`: `api_base` + - `EMBEDDINGS_AZURE_DEPLOYMENT_ID`: `deployment_id` + - `EMBEDDINGS_AZURE_API_VERSION`: `api_version` + - `EMBEDDINGS_AZURE_MODEL_NAME`: `model_name` + - `EMBEDDINGS_AZURE_API_TYPE`: `api_type` + - `EMBEDDINGS_AZURE_DIMENSIONS`: `dimensions` + + + + ```python main.py + from crewai.rag.embeddings.providers.google.types import GenerativeAiProviderSpec + + embedding_model: GenerativeAiProviderSpec = { + "provider": "google-generativeai", + "config": { + "api_key": "your-api-key", + "model_name": "gemini-embedding-001", + "task_type": "RETRIEVAL_DOCUMENT" + } + } + ``` + + **구성 옵션:** + - `api_key` (str): Google AI API 키 + - `model_name` (str): 모델 이름. 기본값: `gemini-embedding-001`. 옵션: `gemini-embedding-001`, `text-embedding-005`, `text-multilingual-embedding-002` + - `task_type` (str): 임베딩 작업 유형. 기본값: `RETRIEVAL_DOCUMENT`. 옵션: `RETRIEVAL_DOCUMENT`, `RETRIEVAL_QUERY` + + **환경 변수:** + - `GOOGLE_API_KEY`, `GEMINI_API_KEY` 또는 `EMBEDDINGS_GOOGLE_API_KEY`: `api_key` + - `EMBEDDINGS_GOOGLE_GENERATIVE_AI_MODEL_NAME`: `model_name` + - `EMBEDDINGS_GOOGLE_GENERATIVE_AI_TASK_TYPE`: `task_type` + + + + ```python main.py + from crewai.rag.embeddings.providers.google.types import VertexAIProviderSpec + + embedding_model: VertexAIProviderSpec = { + "provider": "google-vertex", + "config": { + "model_name": "text-embedding-004", + "project_id": "your-project-id", + "region": "us-central1", + "api_key": "your-api-key" + } + } + ``` + + **구성 옵션:** + - `model_name` (str): 모델 이름. 기본값: `textembedding-gecko`. 옵션: `text-embedding-004`, `textembedding-gecko`, `textembedding-gecko-multilingual` + - `project_id` (str): Google Cloud 프로젝트 ID. 기본값: `cloud-large-language-models` + - `region` (str): Google Cloud 리전. 기본값: `us-central1` + - `api_key` (str): 인증을 위한 API 키 + + **환경 변수:** + - `GOOGLE_APPLICATION_CREDENTIALS`: 서비스 계정 JSON 파일 경로 + - `GOOGLE_CLOUD_PROJECT` 또는 `EMBEDDINGS_GOOGLE_VERTEX_PROJECT_ID`: `project_id` + - `EMBEDDINGS_GOOGLE_VERTEX_MODEL_NAME`: `model_name` + - `EMBEDDINGS_GOOGLE_VERTEX_REGION`: `region` + - `EMBEDDINGS_GOOGLE_VERTEX_API_KEY`: `api_key` + + + + ```python main.py + from crewai.rag.embeddings.providers.jina.types import JinaProviderSpec + + embedding_model: JinaProviderSpec = { + "provider": "jina", + "config": { + "api_key": "your-api-key", + "model_name": "jina-embeddings-v3" + } + } + ``` + + **구성 옵션:** + - `api_key` (str): Jina AI API 키 + - `model_name` (str): 모델 이름. 기본값: `jina-embeddings-v2-base-en`. 옵션: `jina-embeddings-v3`, `jina-embeddings-v2-base-en`, `jina-embeddings-v2-small-en` + + **환경 변수:** + - `JINA_API_KEY` 또는 `EMBEDDINGS_JINA_API_KEY`: `api_key` + - `EMBEDDINGS_JINA_MODEL_NAME`: `model_name` + + + + ```python main.py + from crewai.rag.embeddings.providers.huggingface.types import HuggingFaceProviderSpec + + embedding_model: HuggingFaceProviderSpec = { + "provider": "huggingface", + "config": { + "url": "https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2" + } + } + ``` + + **구성 옵션:** + - `url` (str): HuggingFace 추론 API 엔드포인트의 전체 URL + + **환경 변수:** + - `HUGGINGFACE_URL` 또는 `EMBEDDINGS_HUGGINGFACE_URL`: `url` + + + + ```python main.py + from crewai.rag.embeddings.providers.instructor.types import InstructorProviderSpec + + embedding_model: InstructorProviderSpec = { + "provider": "instructor", + "config": { + "model_name": "hkunlp/instructor-xl", + "device": "cuda", + "instruction": "Represent the document" + } + } + ``` + + **구성 옵션:** + - `model_name` (str): HuggingFace 모델 ID. 기본값: `hkunlp/instructor-base`. 옵션: `hkunlp/instructor-xl`, `hkunlp/instructor-large`, `hkunlp/instructor-base` + - `device` (str): 실행할 장치. 기본값: `cpu`. 옵션: `cpu`, `cuda`, `mps`, `xpu` + - `instruction` (str): 임베딩을 위한 명령어 접두사 + + **환경 변수:** + - `EMBEDDINGS_INSTRUCTOR_MODEL_NAME`: `model_name` + - `EMBEDDINGS_INSTRUCTOR_DEVICE`: `device` + - `EMBEDDINGS_INSTRUCTOR_INSTRUCTION`: `instruction` + + + + ```python main.py + from crewai.rag.embeddings.providers.sentence_transformer.types import SentenceTransformerProviderSpec + + embedding_model: SentenceTransformerProviderSpec = { + "provider": "sentence-transformer", + "config": { + "model_name": "all-mpnet-base-v2", + "device": "cuda", + "normalize_embeddings": True + } + } + ``` + + **구성 옵션:** + - `model_name` (str): Sentence Transformers 모델 이름. 기본값: `all-MiniLM-L6-v2`. 옵션: `all-mpnet-base-v2`, `all-MiniLM-L6-v2`, `paraphrase-multilingual-MiniLM-L12-v2` + - `device` (str): 실행할 장치. 기본값: `cpu`. 옵션: `cpu`, `cuda`, `mps`, `xpu` + - `normalize_embeddings` (bool): 임베딩을 정규화할지 여부. 기본값: `False` + + **환경 변수:** + - `EMBEDDINGS_SENTENCE_TRANSFORMER_MODEL_NAME`: `model_name` + - `EMBEDDINGS_SENTENCE_TRANSFORMER_DEVICE`: `device` + - `EMBEDDINGS_SENTENCE_TRANSFORMER_NORMALIZE_EMBEDDINGS`: `normalize_embeddings` + + + + ```python main.py + from crewai.rag.embeddings.providers.onnx.types import ONNXProviderSpec + + embedding_model: ONNXProviderSpec = { + "provider": "onnx", + "config": { + "preferred_providers": ["CUDAExecutionProvider", "CPUExecutionProvider"] + } + } + ``` + + **구성 옵션:** + - `preferred_providers` (list[str]): 선호도 순으로 나열한 ONNX 실행 프로바이더 목록 + + **환경 변수:** + - `EMBEDDINGS_ONNX_PREFERRED_PROVIDERS`: `preferred_providers`(쉼표로 구분된 목록) + + + + ```python main.py + from crewai.rag.embeddings.providers.openclip.types import OpenCLIPProviderSpec + + embedding_model: OpenCLIPProviderSpec = { + "provider": "openclip", + "config": { + "model_name": "ViT-B-32", + "checkpoint": "laion2b_s34b_b79k", + "device": "cuda" + } + } + ``` + + **구성 옵션:** + - `model_name` (str): OpenCLIP 모델 아키텍처. 기본값: `ViT-B-32`. 옵션: `ViT-B-32`, `ViT-B-16`, `ViT-L-14` + - `checkpoint` (str): 사전 학습된 체크포인트 이름. 기본값: `laion2b_s34b_b79k`. 옵션: `laion2b_s34b_b79k`, `laion400m_e32`, `openai` + - `device` (str): 실행할 장치. 기본값: `cpu`. 옵션: `cpu`, `cuda` + + **환경 변수:** + - `EMBEDDINGS_OPENCLIP_MODEL_NAME`: `model_name` + - `EMBEDDINGS_OPENCLIP_CHECKPOINT`: `checkpoint` + - `EMBEDDINGS_OPENCLIP_DEVICE`: `device` + + + + ```python main.py + from crewai.rag.embeddings.providers.text2vec.types import Text2VecProviderSpec + + embedding_model: Text2VecProviderSpec = { + "provider": "text2vec", + "config": { + "model_name": "shibing624/text2vec-base-multilingual" + } + } + ``` + + **구성 옵션:** + - `model_name` (str): HuggingFace의 Text2Vec 모델 이름. 기본값: `shibing624/text2vec-base-chinese`. 옵션: `shibing624/text2vec-base-multilingual`, `shibing624/text2vec-base-chinese` + + **환경 변수:** + - `EMBEDDINGS_TEXT2VEC_MODEL_NAME`: `model_name` + + + + ```python main.py + from crewai.rag.embeddings.providers.roboflow.types import RoboflowProviderSpec + + embedding_model: RoboflowProviderSpec = { + "provider": "roboflow", + "config": { + "api_key": "your-api-key", + "api_url": "https://infer.roboflow.com" + } + } + ``` + + **구성 옵션:** + - `api_key` (str): Roboflow API 키. 기본값: `""`(빈 문자열) + - `api_url` (str): Roboflow 추론 API URL. 기본값: `https://infer.roboflow.com` + + **환경 변수:** + - `ROBOFLOW_API_KEY` 또는 `EMBEDDINGS_ROBOFLOW_API_KEY`: `api_key` + - `ROBOFLOW_API_URL` 또는 `EMBEDDINGS_ROBOFLOW_API_URL`: `api_url` + + + + ```python main.py + from crewai.rag.embeddings.providers.ibm.types import WatsonXProviderSpec + + embedding_model: WatsonXProviderSpec = { + "provider": "watsonx", + "config": { + "model_id": "ibm/slate-125m-english-rtrvr", + "url": "https://us-south.ml.cloud.ibm.com", + "api_key": "your-api-key", + "project_id": "your-project-id", + "batch_size": 100, + "concurrency_limit": 10, + "persistent_connection": True + } + } + ``` + + **구성 옵션:** + - `model_id` (str): WatsonX 모델 식별자 + - `url` (str): WatsonX API 엔드포인트 + - `api_key` (str): IBM Cloud API 키 + - `project_id` (str): WatsonX 프로젝트 ID + - `space_id` (str): WatsonX 공간 ID(project_id의 대안) + - `batch_size` (int): 임베딩 배치 크기. 기본값: `100` + - `concurrency_limit` (int): 최대 동시 요청 수. 기본값: `10` + - `persistent_connection` (bool): 지속 연결 사용 여부. 기본값: `True` + - 그 외 20개 이상의 추가 인증 및 구성 옵션 + + **환경 변수:** + - `WATSONX_API_KEY` 또는 `EMBEDDINGS_WATSONX_API_KEY`: `api_key` + - `WATSONX_URL` 또는 `EMBEDDINGS_WATSONX_URL`: `url` + - `WATSONX_PROJECT_ID` 또는 `EMBEDDINGS_WATSONX_PROJECT_ID`: `project_id` + - `EMBEDDINGS_WATSONX_MODEL_ID`: `model_id` + - `EMBEDDINGS_WATSONX_SPACE_ID`: `space_id` + - `EMBEDDINGS_WATSONX_BATCH_SIZE`: `batch_size` + - `EMBEDDINGS_WATSONX_CONCURRENCY_LIMIT`: `concurrency_limit` + - `EMBEDDINGS_WATSONX_PERSISTENT_CONNECTION`: `persistent_connection` + + + + ```python main.py + from crewai.rag.core.base_embeddings_callable import EmbeddingFunction + from crewai.rag.embeddings.providers.custom.types import CustomProviderSpec + + class MyEmbeddingFunction(EmbeddingFunction): + def __call__(self, input): + # Your custom embedding logic + return embeddings + + embedding_model: CustomProviderSpec = { + "provider": "custom", + "config": { + "embedding_callable": MyEmbeddingFunction + } + } + ``` + + **구성 옵션:** + - `embedding_callable` (type[EmbeddingFunction]): 사용자 지정 임베딩 함수 클래스 + + **참고:** 사용자 지정 임베딩 함수는 `crewai.rag.core.base_embeddings_callable`에 정의된 `EmbeddingFunction` 프로토콜을 구현해야 합니다. `__call__` 메서드는 입력 데이터를 받아 numpy 배열 목록(또는 정규화할 수 있는 호환 형식)으로 임베딩을 반환해야 합니다. 반환된 임베딩은 자동으로 정규화되고 검증됩니다. + + + +### 참고 +- **필수**로 표시되지 않은 모든 구성 필드는 선택 사항입니다. +- 일반적으로 API 키는 구성 대신 환경 변수를 통해 제공할 수 있습니다. +- 해당하는 경우 기본값이 표시됩니다. + ## 결론 `RagTool`은 다양한 데이터 소스에서 지식 베이스를 생성하고 질의할 수 있는 강력한 방법을 제공합니다. Retrieval-Augmented Generation을 활용하여, 에이전트가 관련 정보를 효율적으로 접근하고 검색할 수 있게 하여, 보다 정확하고 상황에 맞는 응답을 제공하는 능력을 향상시킵니다. diff --git a/docs/edge/pt-BR/tools/ai-ml/ragtool.mdx b/docs/edge/pt-BR/tools/ai-ml/ragtool.mdx index 74b854d7f..61bfe986c 100644 --- a/docs/edge/pt-BR/tools/ai-ml/ragtool.mdx +++ b/docs/edge/pt-BR/tools/ai-ml/ragtool.mdx @@ -9,7 +9,7 @@ mode: "wide" ## Descrição -O `RagTool` foi desenvolvido para responder perguntas aproveitando o poder da Geração Aumentada por Recuperação (RAG) através do EmbedChain. +O `RagTool` foi desenvolvido para responder perguntas aproveitando o poder da Geração Aumentada por Recuperação (RAG) por meio do sistema RAG nativo da CrewAI. Ele fornece uma base de conhecimento dinâmica que pode ser consultada para recuperar informações relevantes de várias fontes de dados. Esta ferramenta é particularmente útil para aplicações que exigem acesso a uma ampla variedade de informações e precisam fornecer respostas contextualmente relevantes. @@ -76,8 +76,8 @@ O `RagTool` pode ser utilizado com uma grande variedade de fontes de dados, incl O `RagTool` aceita os seguintes parâmetros: - **summarize**: Opcional. Indica se o conteúdo recuperado deve ser resumido. O padrão é `False`. -- **adapter**: Opcional. Um adaptador personalizado para a base de conhecimento. Se não for fornecido, será utilizado o EmbedchainAdapter. -- **config**: Opcional. Configuração para o aplicativo EmbedChain subjacente. +- **adapter**: Opcional. Um adaptador personalizado para a base de conhecimento. Se não for fornecido, será utilizado o CrewAIRagAdapter. +- **config**: Opcional. Configuração do sistema RAG subjacente da CrewAI. Aceita um TypedDict `RagToolConfig` com as chaves opcionais `embedding_model` (ProviderSpec) e `vectordb` (VectorDbConfig). Todos os valores de configuração fornecidos programaticamente têm precedência sobre as variáveis de ambiente. ## Adicionando Conteúdo @@ -127,47 +127,528 @@ def knowledge_expert(self) -> Agent: ```python Code from crewai_tools import RagTool +from crewai_tools.tools.rag import RagToolConfig, VectorDbConfig, ProviderSpec # Create a RAG tool with custom configuration -config = { - "app": { - "name": "custom_app", - }, - "llm": { - "provider": "openai", - "config": { - "model": "gpt-4", - } - }, - "embedding_model": { - "provider": "openai", - "config": { - "model": "text-embedding-ada-002" - } - }, - "vectordb": { - "provider": "elasticsearch", - "config": { - "collection_name": "my-collection", - "cloud_id": "deployment-name:xxxx", - "api_key": "your-key", - "verify_certs": False - } - }, - "chunker": { - "chunk_size": 400, - "chunk_overlap": 100, - "length_function": "len", - "min_chunk_size": 0 + +vectordb: VectorDbConfig = { + "provider": "qdrant", + "config": { + "collection_name": "my-collection" } } +embedding_model: ProviderSpec = { + "provider": "openai", + "config": { + "model_name": "text-embedding-3-small" + } +} + +config: RagToolConfig = { + "vectordb": vectordb, + "embedding_model": embedding_model +} + rag_tool = RagTool(config=config, summarize=True) ``` -A ferramenta RAG interna utiliza o adaptador Embedchain, possibilitando que você forneça quaisquer opções de configuração suportadas pelo Embedchain. -Você pode consultar a [documentação do Embedchain](https://docs.embedchain.ai/components/introduction) para mais detalhes. -Certifique-se de revisar as opções de configuração disponíveis no arquivo .yaml. +## Configuração do Modelo de Embedding + +O parâmetro `embedding_model` aceita um dicionário `crewai.rag.embeddings.types.ProviderSpec` com a seguinte estrutura: + +```python +{ + "provider": "provider-name", # Required + "config": { # Optional + # Provider-specific configuration + } +} +``` + +### Provedores Suportados + + + + ```python main.py + from crewai.rag.embeddings.providers.openai.types import OpenAIProviderSpec + + embedding_model: OpenAIProviderSpec = { + "provider": "openai", + "config": { + "api_key": "your-api-key", + "model_name": "text-embedding-ada-002", + "dimensions": 1536, + "organization_id": "your-org-id", + "api_base": "https://api.openai.com/v1", + "api_version": "v1", + "default_headers": {"Custom-Header": "value"} + } + } + ``` + + **Opções de Configuração:** + - `api_key` (str): Chave da API OpenAI + - `model_name` (str): Modelo a ser utilizado. Padrão: `text-embedding-ada-002`. Opções: `text-embedding-3-small`, `text-embedding-3-large`, `text-embedding-ada-002` + - `dimensions` (int): Número de dimensões do embedding + - `organization_id` (str): ID da organização OpenAI + - `api_base` (str): URL base personalizada da API + - `api_version` (str): Versão da API + - `default_headers` (dict): Cabeçalhos personalizados para solicitações à API + + **Variáveis de Ambiente:** + - `OPENAI_API_KEY` ou `EMBEDDINGS_OPENAI_API_KEY`: `api_key` + - `OPENAI_ORGANIZATION_ID` ou `EMBEDDINGS_OPENAI_ORGANIZATION_ID`: `organization_id` + - `OPENAI_MODEL_NAME` ou `EMBEDDINGS_OPENAI_MODEL_NAME`: `model_name` + - `OPENAI_API_BASE` ou `EMBEDDINGS_OPENAI_API_BASE`: `api_base` + - `OPENAI_API_VERSION` ou `EMBEDDINGS_OPENAI_API_VERSION`: `api_version` + - `OPENAI_DIMENSIONS` ou `EMBEDDINGS_OPENAI_DIMENSIONS`: `dimensions` + + + + ```python main.py + from crewai.rag.embeddings.providers.cohere.types import CohereProviderSpec + + embedding_model: CohereProviderSpec = { + "provider": "cohere", + "config": { + "api_key": "your-api-key", + "model_name": "embed-english-v3.0" + } + } + ``` + + **Opções de Configuração:** + - `api_key` (str): Chave da API Cohere + - `model_name` (str): Modelo a ser utilizado. Padrão: `large`. Opções: `embed-english-v3.0`, `embed-multilingual-v3.0`, `large`, `small` + + **Variáveis de Ambiente:** + - `COHERE_API_KEY` ou `EMBEDDINGS_COHERE_API_KEY`: `api_key` + - `EMBEDDINGS_COHERE_MODEL_NAME`: `model_name` + + + + ```python main.py + from crewai.rag.embeddings.providers.voyageai.types import VoyageAIProviderSpec + + embedding_model: VoyageAIProviderSpec = { + "provider": "voyageai", + "config": { + "api_key": "your-api-key", + "model": "voyage-3", + "input_type": "document", + "truncation": True, + "output_dtype": "float32", + "output_dimension": 1024, + "max_retries": 3, + "timeout": 60.0 + } + } + ``` + + **Opções de Configuração:** + - `api_key` (str): Chave da API VoyageAI + - `model` (str): Modelo a ser utilizado. Padrão: `voyage-2`. Opções: `voyage-3`, `voyage-3-lite`, `voyage-code-3`, `voyage-large-2` + - `input_type` (str): Tipo de entrada. Opções: `document` (para armazenamento), `query` (para pesquisa) + - `truncation` (bool): Indica se entradas que excedem o comprimento máximo devem ser truncadas. Padrão: `True` + - `output_dtype` (str): Tipo de dados da saída + - `output_dimension` (int): Dimensão dos embeddings de saída + - `max_retries` (int): Número máximo de tentativas. Padrão: `0` + - `timeout` (float): Tempo limite da solicitação em segundos + + **Variáveis de Ambiente:** + - `VOYAGEAI_API_KEY` ou `EMBEDDINGS_VOYAGEAI_API_KEY`: `api_key` + - `VOYAGEAI_MODEL` ou `EMBEDDINGS_VOYAGEAI_MODEL`: `model` + - `VOYAGEAI_INPUT_TYPE` ou `EMBEDDINGS_VOYAGEAI_INPUT_TYPE`: `input_type` + - `VOYAGEAI_TRUNCATION` ou `EMBEDDINGS_VOYAGEAI_TRUNCATION`: `truncation` + - `VOYAGEAI_OUTPUT_DTYPE` ou `EMBEDDINGS_VOYAGEAI_OUTPUT_DTYPE`: `output_dtype` + - `VOYAGEAI_OUTPUT_DIMENSION` ou `EMBEDDINGS_VOYAGEAI_OUTPUT_DIMENSION`: `output_dimension` + - `VOYAGEAI_MAX_RETRIES` ou `EMBEDDINGS_VOYAGEAI_MAX_RETRIES`: `max_retries` + - `VOYAGEAI_TIMEOUT` ou `EMBEDDINGS_VOYAGEAI_TIMEOUT`: `timeout` + + + + ```python main.py + from crewai.rag.embeddings.providers.ollama.types import OllamaProviderSpec + + embedding_model: OllamaProviderSpec = { + "provider": "ollama", + "config": { + "model_name": "llama2", + "url": "http://localhost:11434/api/embeddings" + } + } + ``` + + **Opções de Configuração:** + - `model_name` (str): Nome do modelo Ollama (por exemplo, `llama2`, `mistral`, `nomic-embed-text`) + - `url` (str): URL do endpoint da API Ollama. Padrão: `http://localhost:11434/api/embeddings` + + **Variáveis de Ambiente:** + - `OLLAMA_MODEL` ou `EMBEDDINGS_OLLAMA_MODEL`: `model_name` + - `OLLAMA_URL` ou `EMBEDDINGS_OLLAMA_URL`: `url` + + + + ```python main.py + from crewai.rag.embeddings.providers.aws.types import BedrockProviderSpec + + embedding_model: BedrockProviderSpec = { + "provider": "amazon-bedrock", + "config": { + "model_name": "amazon.titan-embed-text-v2:0", + "session": boto3_session + } + } + ``` + + **Opções de Configuração:** + - `model_name` (str): ID do modelo Bedrock. Padrão: `amazon.titan-embed-text-v1`. Opções: `amazon.titan-embed-text-v1`, `amazon.titan-embed-text-v2:0`, `cohere.embed-english-v3`, `cohere.embed-multilingual-v3` + - `session` (Any): Objeto de sessão Boto3 para autenticação da AWS + + **Variáveis de Ambiente:** + - `AWS_ACCESS_KEY_ID`: Chave de acesso da AWS + - `AWS_SECRET_ACCESS_KEY`: Chave secreta da AWS + - `AWS_REGION`: Região da AWS (por exemplo, `us-east-1`) + + + + ```python main.py + from crewai.rag.embeddings.providers.microsoft.types import AzureProviderSpec + + embedding_model: AzureProviderSpec = { + "provider": "azure", + "config": { + "deployment_id": "your-deployment-id", + "api_key": "your-api-key", + "api_base": "https://your-resource.openai.azure.com", + "api_version": "2024-02-01", + "model_name": "text-embedding-ada-002", + "api_type": "azure" + } + } + ``` + + **Opções de Configuração:** + - `deployment_id` (str): **Obrigatório** - ID de implantação do Azure OpenAI + - `api_key` (str): Chave da API Azure OpenAI + - `api_base` (str): Endpoint do recurso Azure OpenAI + - `api_version` (str): Versão da API. Exemplo: `2024-02-01` + - `model_name` (str): Nome do modelo. Padrão: `text-embedding-ada-002` + - `api_type` (str): Tipo de API. Padrão: `azure` + - `dimensions` (int): Dimensões da saída + - `default_headers` (dict): Cabeçalhos personalizados + + **Variáveis de Ambiente:** + - `AZURE_OPENAI_API_KEY` ou `EMBEDDINGS_AZURE_API_KEY`: `api_key` + - `AZURE_OPENAI_ENDPOINT` ou `EMBEDDINGS_AZURE_API_BASE`: `api_base` + - `EMBEDDINGS_AZURE_DEPLOYMENT_ID`: `deployment_id` + - `EMBEDDINGS_AZURE_API_VERSION`: `api_version` + - `EMBEDDINGS_AZURE_MODEL_NAME`: `model_name` + - `EMBEDDINGS_AZURE_API_TYPE`: `api_type` + - `EMBEDDINGS_AZURE_DIMENSIONS`: `dimensions` + + + + ```python main.py + from crewai.rag.embeddings.providers.google.types import GenerativeAiProviderSpec + + embedding_model: GenerativeAiProviderSpec = { + "provider": "google-generativeai", + "config": { + "api_key": "your-api-key", + "model_name": "gemini-embedding-001", + "task_type": "RETRIEVAL_DOCUMENT" + } + } + ``` + + **Opções de Configuração:** + - `api_key` (str): Chave da API Google AI + - `model_name` (str): Nome do modelo. Padrão: `gemini-embedding-001`. Opções: `gemini-embedding-001`, `text-embedding-005`, `text-multilingual-embedding-002` + - `task_type` (str): Tipo de tarefa para embeddings. Padrão: `RETRIEVAL_DOCUMENT`. Opções: `RETRIEVAL_DOCUMENT`, `RETRIEVAL_QUERY` + + **Variáveis de Ambiente:** + - `GOOGLE_API_KEY`, `GEMINI_API_KEY` ou `EMBEDDINGS_GOOGLE_API_KEY`: `api_key` + - `EMBEDDINGS_GOOGLE_GENERATIVE_AI_MODEL_NAME`: `model_name` + - `EMBEDDINGS_GOOGLE_GENERATIVE_AI_TASK_TYPE`: `task_type` + + + + ```python main.py + from crewai.rag.embeddings.providers.google.types import VertexAIProviderSpec + + embedding_model: VertexAIProviderSpec = { + "provider": "google-vertex", + "config": { + "model_name": "text-embedding-004", + "project_id": "your-project-id", + "region": "us-central1", + "api_key": "your-api-key" + } + } + ``` + + **Opções de Configuração:** + - `model_name` (str): Nome do modelo. Padrão: `textembedding-gecko`. Opções: `text-embedding-004`, `textembedding-gecko`, `textembedding-gecko-multilingual` + - `project_id` (str): ID do projeto Google Cloud. Padrão: `cloud-large-language-models` + - `region` (str): Região do Google Cloud. Padrão: `us-central1` + - `api_key` (str): Chave de API para autenticação + + **Variáveis de Ambiente:** + - `GOOGLE_APPLICATION_CREDENTIALS`: Caminho para o arquivo JSON da conta de serviço + - `GOOGLE_CLOUD_PROJECT` ou `EMBEDDINGS_GOOGLE_VERTEX_PROJECT_ID`: `project_id` + - `EMBEDDINGS_GOOGLE_VERTEX_MODEL_NAME`: `model_name` + - `EMBEDDINGS_GOOGLE_VERTEX_REGION`: `region` + - `EMBEDDINGS_GOOGLE_VERTEX_API_KEY`: `api_key` + + + + ```python main.py + from crewai.rag.embeddings.providers.jina.types import JinaProviderSpec + + embedding_model: JinaProviderSpec = { + "provider": "jina", + "config": { + "api_key": "your-api-key", + "model_name": "jina-embeddings-v3" + } + } + ``` + + **Opções de Configuração:** + - `api_key` (str): Chave da API Jina AI + - `model_name` (str): Nome do modelo. Padrão: `jina-embeddings-v2-base-en`. Opções: `jina-embeddings-v3`, `jina-embeddings-v2-base-en`, `jina-embeddings-v2-small-en` + + **Variáveis de Ambiente:** + - `JINA_API_KEY` ou `EMBEDDINGS_JINA_API_KEY`: `api_key` + - `EMBEDDINGS_JINA_MODEL_NAME`: `model_name` + + + + ```python main.py + from crewai.rag.embeddings.providers.huggingface.types import HuggingFaceProviderSpec + + embedding_model: HuggingFaceProviderSpec = { + "provider": "huggingface", + "config": { + "url": "https://api-inference.huggingface.co/models/sentence-transformers/all-MiniLM-L6-v2" + } + } + ``` + + **Opções de Configuração:** + - `url` (str): URL completa do endpoint da API de inferência do HuggingFace + + **Variáveis de Ambiente:** + - `HUGGINGFACE_URL` ou `EMBEDDINGS_HUGGINGFACE_URL`: `url` + + + + ```python main.py + from crewai.rag.embeddings.providers.instructor.types import InstructorProviderSpec + + embedding_model: InstructorProviderSpec = { + "provider": "instructor", + "config": { + "model_name": "hkunlp/instructor-xl", + "device": "cuda", + "instruction": "Represent the document" + } + } + ``` + + **Opções de Configuração:** + - `model_name` (str): ID do modelo HuggingFace. Padrão: `hkunlp/instructor-base`. Opções: `hkunlp/instructor-xl`, `hkunlp/instructor-large`, `hkunlp/instructor-base` + - `device` (str): Dispositivo no qual executar. Padrão: `cpu`. Opções: `cpu`, `cuda`, `mps`, `xpu` + - `instruction` (str): Prefixo de instrução para embeddings + + **Variáveis de Ambiente:** + - `EMBEDDINGS_INSTRUCTOR_MODEL_NAME`: `model_name` + - `EMBEDDINGS_INSTRUCTOR_DEVICE`: `device` + - `EMBEDDINGS_INSTRUCTOR_INSTRUCTION`: `instruction` + + + + ```python main.py + from crewai.rag.embeddings.providers.sentence_transformer.types import SentenceTransformerProviderSpec + + embedding_model: SentenceTransformerProviderSpec = { + "provider": "sentence-transformer", + "config": { + "model_name": "all-mpnet-base-v2", + "device": "cuda", + "normalize_embeddings": True + } + } + ``` + + **Opções de Configuração:** + - `model_name` (str): Nome do modelo Sentence Transformers. Padrão: `all-MiniLM-L6-v2`. Opções: `all-mpnet-base-v2`, `all-MiniLM-L6-v2`, `paraphrase-multilingual-MiniLM-L12-v2` + - `device` (str): Dispositivo no qual executar. Padrão: `cpu`. Opções: `cpu`, `cuda`, `mps`, `xpu` + - `normalize_embeddings` (bool): Indica se os embeddings devem ser normalizados. Padrão: `False` + + **Variáveis de Ambiente:** + - `EMBEDDINGS_SENTENCE_TRANSFORMER_MODEL_NAME`: `model_name` + - `EMBEDDINGS_SENTENCE_TRANSFORMER_DEVICE`: `device` + - `EMBEDDINGS_SENTENCE_TRANSFORMER_NORMALIZE_EMBEDDINGS`: `normalize_embeddings` + + + + ```python main.py + from crewai.rag.embeddings.providers.onnx.types import ONNXProviderSpec + + embedding_model: ONNXProviderSpec = { + "provider": "onnx", + "config": { + "preferred_providers": ["CUDAExecutionProvider", "CPUExecutionProvider"] + } + } + ``` + + **Opções de Configuração:** + - `preferred_providers` (list[str]): Lista de provedores de execução ONNX em ordem de preferência + + **Variáveis de Ambiente:** + - `EMBEDDINGS_ONNX_PREFERRED_PROVIDERS`: `preferred_providers` (lista separada por vírgulas) + + + + ```python main.py + from crewai.rag.embeddings.providers.openclip.types import OpenCLIPProviderSpec + + embedding_model: OpenCLIPProviderSpec = { + "provider": "openclip", + "config": { + "model_name": "ViT-B-32", + "checkpoint": "laion2b_s34b_b79k", + "device": "cuda" + } + } + ``` + + **Opções de Configuração:** + - `model_name` (str): Arquitetura do modelo OpenCLIP. Padrão: `ViT-B-32`. Opções: `ViT-B-32`, `ViT-B-16`, `ViT-L-14` + - `checkpoint` (str): Nome do checkpoint pré-treinado. Padrão: `laion2b_s34b_b79k`. Opções: `laion2b_s34b_b79k`, `laion400m_e32`, `openai` + - `device` (str): Dispositivo no qual executar. Padrão: `cpu`. Opções: `cpu`, `cuda` + + **Variáveis de Ambiente:** + - `EMBEDDINGS_OPENCLIP_MODEL_NAME`: `model_name` + - `EMBEDDINGS_OPENCLIP_CHECKPOINT`: `checkpoint` + - `EMBEDDINGS_OPENCLIP_DEVICE`: `device` + + + + ```python main.py + from crewai.rag.embeddings.providers.text2vec.types import Text2VecProviderSpec + + embedding_model: Text2VecProviderSpec = { + "provider": "text2vec", + "config": { + "model_name": "shibing624/text2vec-base-multilingual" + } + } + ``` + + **Opções de Configuração:** + - `model_name` (str): Nome do modelo Text2Vec do HuggingFace. Padrão: `shibing624/text2vec-base-chinese`. Opções: `shibing624/text2vec-base-multilingual`, `shibing624/text2vec-base-chinese` + + **Variáveis de Ambiente:** + - `EMBEDDINGS_TEXT2VEC_MODEL_NAME`: `model_name` + + + + ```python main.py + from crewai.rag.embeddings.providers.roboflow.types import RoboflowProviderSpec + + embedding_model: RoboflowProviderSpec = { + "provider": "roboflow", + "config": { + "api_key": "your-api-key", + "api_url": "https://infer.roboflow.com" + } + } + ``` + + **Opções de Configuração:** + - `api_key` (str): Chave da API Roboflow. Padrão: `""` (string vazia) + - `api_url` (str): URL da API de inferência do Roboflow. Padrão: `https://infer.roboflow.com` + + **Variáveis de Ambiente:** + - `ROBOFLOW_API_KEY` ou `EMBEDDINGS_ROBOFLOW_API_KEY`: `api_key` + - `ROBOFLOW_API_URL` ou `EMBEDDINGS_ROBOFLOW_API_URL`: `api_url` + + + + ```python main.py + from crewai.rag.embeddings.providers.ibm.types import WatsonXProviderSpec + + embedding_model: WatsonXProviderSpec = { + "provider": "watsonx", + "config": { + "model_id": "ibm/slate-125m-english-rtrvr", + "url": "https://us-south.ml.cloud.ibm.com", + "api_key": "your-api-key", + "project_id": "your-project-id", + "batch_size": 100, + "concurrency_limit": 10, + "persistent_connection": True + } + } + ``` + + **Opções de Configuração:** + - `model_id` (str): Identificador do modelo WatsonX + - `url` (str): Endpoint da API WatsonX + - `api_key` (str): Chave da API IBM Cloud + - `project_id` (str): ID do projeto WatsonX + - `space_id` (str): ID do espaço WatsonX (alternativa ao project_id) + - `batch_size` (int): Tamanho do lote para embeddings. Padrão: `100` + - `concurrency_limit` (int): Número máximo de solicitações simultâneas. Padrão: `10` + - `persistent_connection` (bool): Utilizar conexões persistentes. Padrão: `True` + - Mais de 20 opções adicionais de autenticação e configuração + + **Variáveis de Ambiente:** + - `WATSONX_API_KEY` ou `EMBEDDINGS_WATSONX_API_KEY`: `api_key` + - `WATSONX_URL` ou `EMBEDDINGS_WATSONX_URL`: `url` + - `WATSONX_PROJECT_ID` ou `EMBEDDINGS_WATSONX_PROJECT_ID`: `project_id` + - `EMBEDDINGS_WATSONX_MODEL_ID`: `model_id` + - `EMBEDDINGS_WATSONX_SPACE_ID`: `space_id` + - `EMBEDDINGS_WATSONX_BATCH_SIZE`: `batch_size` + - `EMBEDDINGS_WATSONX_CONCURRENCY_LIMIT`: `concurrency_limit` + - `EMBEDDINGS_WATSONX_PERSISTENT_CONNECTION`: `persistent_connection` + + + + ```python main.py + from crewai.rag.core.base_embeddings_callable import EmbeddingFunction + from crewai.rag.embeddings.providers.custom.types import CustomProviderSpec + + class MyEmbeddingFunction(EmbeddingFunction): + def __call__(self, input): + # Your custom embedding logic + return embeddings + + embedding_model: CustomProviderSpec = { + "provider": "custom", + "config": { + "embedding_callable": MyEmbeddingFunction + } + } + ``` + + **Opções de Configuração:** + - `embedding_callable` (type[EmbeddingFunction]): Classe da função de embedding personalizada + + **Observação:** As funções de embedding personalizadas devem implementar o protocolo `EmbeddingFunction` definido em `crewai.rag.core.base_embeddings_callable`. O método `__call__` deve aceitar dados de entrada e retornar embeddings como uma lista de arrays numpy (ou um formato compatível que será normalizado). Os embeddings retornados são normalizados e validados automaticamente. + + + +### Observações +- Todos os campos de configuração são opcionais, a menos que estejam marcados como **Obrigatório** +- Normalmente, as chaves de API podem ser fornecidas por meio de variáveis de ambiente em vez da configuração +- Os valores padrão são exibidos quando aplicável + ## Conclusão -O `RagTool` oferece uma maneira poderosa de criar e consultar bases de conhecimento a partir de diversas fontes de dados. Ao explorar a Geração Aumentada por Recuperação, ele permite que agentes acessem e recuperem informações relevantes de forma eficiente, ampliando a capacidade de fornecer respostas precisas e contextualmente apropriadas. \ No newline at end of file +O `RagTool` oferece uma maneira poderosa de criar e consultar bases de conhecimento a partir de diversas fontes de dados. Ao explorar a Geração Aumentada por Recuperação, ele permite que agentes acessem e recuperem informações relevantes de forma eficiente, ampliando a capacidade de fornecer respostas precisas e contextualmente apropriadas. diff --git a/lib/crewai/src/crewai/rag/embeddings/providers/instructor/instructor_provider.py b/lib/crewai/src/crewai/rag/embeddings/providers/instructor/instructor_provider.py index d569d989d..a403de39e 100644 --- a/lib/crewai/src/crewai/rag/embeddings/providers/instructor/instructor_provider.py +++ b/lib/crewai/src/crewai/rag/embeddings/providers/instructor/instructor_provider.py @@ -26,7 +26,7 @@ class InstructorProvider(BaseEmbeddingsProvider[InstructorEmbeddingFunction]): ) device: str = Field( default="cpu", - description="Device to run model on (cpu or cuda)", + description="Device to run model on (e.g., cpu, cuda, mps, xpu)", validation_alias=AliasChoices( "EMBEDDINGS_INSTRUCTOR_DEVICE", "INSTRUCTOR_DEVICE" ), diff --git a/lib/crewai/src/crewai/rag/embeddings/providers/sentence_transformer/sentence_transformer_provider.py b/lib/crewai/src/crewai/rag/embeddings/providers/sentence_transformer/sentence_transformer_provider.py index c045b4ebd..7aa33b31f 100644 --- a/lib/crewai/src/crewai/rag/embeddings/providers/sentence_transformer/sentence_transformer_provider.py +++ b/lib/crewai/src/crewai/rag/embeddings/providers/sentence_transformer/sentence_transformer_provider.py @@ -28,7 +28,7 @@ class SentenceTransformerProvider( ) device: str = Field( default="cpu", - description="Device to run model on (cpu or cuda)", + description="Device to run model on (e.g., cpu, cuda, mps, xpu)", validation_alias=AliasChoices( "EMBEDDINGS_SENTENCE_TRANSFORMER_DEVICE", "SENTENCE_TRANSFORMER_DEVICE" ), diff --git a/lib/crewai/tests/rag/embeddings/test_backward_compatibility.py b/lib/crewai/tests/rag/embeddings/test_backward_compatibility.py index 91181273a..8cfb8f301 100644 --- a/lib/crewai/tests/rag/embeddings/test_backward_compatibility.py +++ b/lib/crewai/tests/rag/embeddings/test_backward_compatibility.py @@ -1,5 +1,7 @@ """Tests for backward compatibility of embedding provider configurations.""" +import pytest + from crewai.rag.embeddings.factory import build_embedder, PROVIDER_PATHS from crewai.rag.embeddings.providers.openai.openai_provider import OpenAIProvider from crewai.rag.embeddings.providers.cohere.cohere_provider import CohereProvider @@ -357,15 +359,19 @@ class TestDocumentationCodeSnippets: ) assert provider.model_name == "jina-embeddings-v3" - def test_ragtool_sentence_transformer_config(self): - """Test RagTool SentenceTransformer config from ragtool.mdx.""" + @pytest.mark.parametrize("device", ["cpu", "cuda", "mps", "xpu"]) + def test_ragtool_sentence_transformer_config(self, device: str): + """Test RagTool SentenceTransformer config from ragtool.mdx. + + Parametrized over documented device strings to confirm each + value is preserved.""" provider = SentenceTransformerProvider( model_name="all-mpnet-base-v2", - device="cuda", + device=device, normalize_embeddings=True, ) assert provider.model_name == "all-mpnet-base-v2" - assert provider.device == "cuda" + assert provider.device == device assert provider.normalize_embeddings is True From 9393a47f313a0544db15693db9bcc48585f30ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Mon, 14 Sep 2026 03:22:08 -0300 Subject: [PATCH 09/11] fix(agents): request the forced final answer as a user turn (#7450) * fix(agents): request the forced final answer as a user turn When an agent reaches max_iter, handle_max_iterations_exceeded appended the "give your best final answer" instruction as an assistant message and relied on assistant prefill to make the model continue it. Current Claude models (Opus 5, Sonnet 5, Fable 5.x, the 4.6+ family) reject a request that ends on an assistant turn with a 400, after the whole iteration budget has already been spent. The instruction is now appended as a user turn, which every provider accepts. The handler's formatted_answer parameter is dropped: every caller had already appended that text as the last assistant message, so prefixing it again only duplicated history. Co-Authored-By: Claude Fable 5.1 * fix(agents): stop the lite agent loop after the forced final answer LiteAgent._invoke_loop fell through after handle_max_iterations_exceeded and issued a regular LLM call on the same history, discarding the forced answer. Break out of the loop the way CrewAgentExecutor already does, and assert a single LLM call in the test. Co-Authored-By: Claude Fable 5.1 --------- Co-authored-by: Claude Fable 5.1 Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> --- .../src/crewai/agents/crew_agent_executor.py | 4 - .../src/crewai/experimental/agent_executor.py | 1 - lib/crewai/src/crewai/lite_agent.py | 2 +- .../src/crewai/utilities/agent_utils.py | 18 ++- lib/crewai/tests/agents/test_agent.py | 98 ++++++++++++++++ .../tests/agents/test_agent_executor.py | 38 +++++++ .../tests/agents/test_async_agent_executor.py | 91 ++++++++++++++- lib/crewai/tests/agents/test_lite_agent.py | 41 +++++++ .../tests/llms/anthropic/test_anthropic.py | 69 ++++++++++++ .../tests/utilities/test_agent_utils.py | 106 ++++++++++++++++++ 10 files changed, 450 insertions(+), 18 deletions(-) diff --git a/lib/crewai/src/crewai/agents/crew_agent_executor.py b/lib/crewai/src/crewai/agents/crew_agent_executor.py index 17ad80a6a..a9d859097 100644 --- a/lib/crewai/src/crewai/agents/crew_agent_executor.py +++ b/lib/crewai/src/crewai/agents/crew_agent_executor.py @@ -364,7 +364,6 @@ class CrewAgentExecutor(BaseAgentExecutor): try: if has_reached_max_iterations(self.iterations, self.max_iter): formatted_answer = handle_max_iterations_exceeded( - formatted_answer, printer=PRINTER, messages=self.messages, llm=cast("BaseLLM", self.llm), @@ -524,7 +523,6 @@ class CrewAgentExecutor(BaseAgentExecutor): try: if has_reached_max_iterations(self.iterations, self.max_iter): formatted_answer = handle_max_iterations_exceeded( - None, printer=PRINTER, messages=self.messages, llm=cast("BaseLLM", self.llm), @@ -1178,7 +1176,6 @@ class CrewAgentExecutor(BaseAgentExecutor): try: if has_reached_max_iterations(self.iterations, self.max_iter): formatted_answer = handle_max_iterations_exceeded( - formatted_answer, printer=PRINTER, messages=self.messages, llm=cast("BaseLLM", self.llm), @@ -1324,7 +1321,6 @@ class CrewAgentExecutor(BaseAgentExecutor): try: if has_reached_max_iterations(self.iterations, self.max_iter): formatted_answer = handle_max_iterations_exceeded( - None, printer=PRINTER, messages=self.messages, llm=cast("BaseLLM", self.llm), diff --git a/lib/crewai/src/crewai/experimental/agent_executor.py b/lib/crewai/src/crewai/experimental/agent_executor.py index d51e7aaca..5977b1727 100644 --- a/lib/crewai/src/crewai/experimental/agent_executor.py +++ b/lib/crewai/src/crewai/experimental/agent_executor.py @@ -1439,7 +1439,6 @@ class AgentExecutor(Flow[AgentExecutorState], BaseAgentExecutor): return "agent_finished" formatted_answer = handle_max_iterations_exceeded( - formatted_answer=None, printer=PRINTER, messages=list(self.state.messages), llm=self.llm, diff --git a/lib/crewai/src/crewai/lite_agent.py b/lib/crewai/src/crewai/lite_agent.py index 48a130956..fe393478b 100644 --- a/lib/crewai/src/crewai/lite_agent.py +++ b/lib/crewai/src/crewai/lite_agent.py @@ -929,13 +929,13 @@ class LiteAgent(FlowTrackable, BaseModel): try: if has_reached_max_iterations(self._iterations, self.max_iterations): formatted_answer = handle_max_iterations_exceeded( - formatted_answer, printer=PRINTER, messages=self._messages, llm=cast(LLM, self.llm), callbacks=self._callbacks, verbose=self.verbose, ) + break enforce_rpm_limit(self.request_within_rpm_limit) diff --git a/lib/crewai/src/crewai/utilities/agent_utils.py b/lib/crewai/src/crewai/utilities/agent_utils.py index b8f664c11..7e6eb3f2d 100644 --- a/lib/crewai/src/crewai/utilities/agent_utils.py +++ b/lib/crewai/src/crewai/utilities/agent_utils.py @@ -374,7 +374,6 @@ def has_reached_max_iterations(iterations: int, max_iterations: int) -> bool: def handle_max_iterations_exceeded( - formatted_answer: AgentAction | AgentFinish | None, printer: Printer, messages: list[LLMMessage], llm: LLM | BaseLLM, @@ -384,9 +383,9 @@ def handle_max_iterations_exceeded( """Handles the case when the maximum number of iterations is exceeded. Performs one more LLM call to get the final answer. Args: - formatted_answer: The last formatted answer from the agent. printer: Printer instance for output. - messages: List of messages to send to the LLM. + messages: Conversation so far; the forced-answer instruction is appended + to it as a user turn. llm: The LLM instance to call. callbacks: List of callbacks for the LLM call. verbose: Whether to print output. @@ -400,14 +399,11 @@ def handle_max_iterations_exceeded( color="yellow", ) - if formatted_answer and hasattr(formatted_answer, "text"): - assistant_message = ( - formatted_answer.text + f"\n{I18N_DEFAULT.errors('force_final_answer')}" - ) - else: - assistant_message = I18N_DEFAULT.errors("force_final_answer") - - messages.append(format_message_for_llm(assistant_message, role="assistant")) + # A trailing assistant turn is a prefill request, which current Claude + # models reject with a 400; every provider accepts a trailing user turn. + messages.append( + format_message_for_llm(I18N_DEFAULT.errors("force_final_answer"), role="user") + ) answer = llm.call( messages, diff --git a/lib/crewai/tests/agents/test_agent.py b/lib/crewai/tests/agents/test_agent.py index 74403ccd8..73408ef11 100644 --- a/lib/crewai/tests/agents/test_agent.py +++ b/lib/crewai/tests/agents/test_agent.py @@ -2889,3 +2889,101 @@ class TestSharedLLMStopWords: assert seen == [{"Original:", "Observation:"}] assert shared.stop == ["Original:"] + + +class TestMaxIterationsForcedAnswer: + """Both sync loops request the forced final answer with a trailing user turn. + + Current Claude models reject a request that ends on an assistant message, + so the nudge must never be sent as assistant prefill. + """ + + @staticmethod + def _make_executor(llm: MagicMock, original_tools: list) -> CrewAgentExecutor: + from crewai.agents.tools_handler import ToolsHandler + + agent = Agent(role="r", goal="g", backstory="b", llm=llm, verbose=False) + task = Task(description="d", expected_output="o", agent=agent) + executor = CrewAgentExecutor( + agent=agent, + task=task, + llm=llm, + crew=None, + prompt={"prompt": "p {input} {tool_names} {tools}"}, + max_iter=1, + tools=[], + original_tools=original_tools, + tools_names="", + stop_words=[], + tools_description="", + tools_handler=ToolsHandler(), + ) + executor.iterations = 1 + return executor + + def test_react_loop_forces_final_answer_with_user_turn(self) -> None: + from crewai.utilities.i18n import I18N_DEFAULT + + llm = MagicMock(spec=LLM) + llm.stop = [] + llm.supports_stop_words.return_value = True + llm.supports_function_calling.return_value = False + llm.call.return_value = "Final Answer: forced" + executor = self._make_executor(llm, original_tools=[]) + executor.messages = [ + {"role": "user", "content": "Collect all the data."}, + {"role": "assistant", "content": "Thought: I need data\nObservation: partial"}, + ] + + with patch.object(executor, "_show_logs"): + result = executor._invoke_loop() + + sent = llm.call.call_args.args[0] + assert sent[-1] == { + "role": "user", + "content": I18N_DEFAULT.errors("force_final_answer"), + } + assert isinstance(result, AgentFinish) + assert result.output == "forced" + + def test_native_tools_loop_forces_final_answer_with_user_turn(self) -> None: + from crewai.utilities.i18n import I18N_DEFAULT + + @tool + def get_data(step: str) -> str: + """Get data for a step.""" + return f"data for {step}" + + llm = MagicMock(spec=LLM) + llm.stop = [] + llm.supports_stop_words.return_value = True + llm.supports_function_calling.return_value = True + llm.call.return_value = "Final Answer: forced" + executor = self._make_executor(llm, original_tools=[get_data]) + executor.messages = [ + {"role": "user", "content": "Collect all the data."}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": "get_data", "arguments": "{}"}, + } + ], + }, + {"role": "tool", "tool_call_id": "call_1", "name": "get_data", "content": "partial"}, + {"role": "user", "content": I18N_DEFAULT.slice("post_tool_reasoning")}, + ] + + with patch.object(executor, "_show_logs"): + result = executor._invoke_loop() + + sent = llm.call.call_args.args[0] + assert sent[-1] == { + "role": "user", + "content": I18N_DEFAULT.errors("force_final_answer"), + } + assert isinstance(result, AgentFinish) + assert result.output == "forced" diff --git a/lib/crewai/tests/agents/test_agent_executor.py b/lib/crewai/tests/agents/test_agent_executor.py index ba025b3e9..f09d4c429 100644 --- a/lib/crewai/tests/agents/test_agent_executor.py +++ b/lib/crewai/tests/agents/test_agent_executor.py @@ -80,6 +80,7 @@ from crewai.utilities.step_execution_context import StepExecutionContext from crewai.utilities.planning_types import TodoItem, TodoList from crewai.utilities.prompts import StandardPromptResult, SystemPromptResult from crewai.utilities.file_store import clear_files, clear_task_files, store_files +from crewai.utilities.i18n import I18N_DEFAULT from crewai_files import TextFile class TestAgentExecutorState: @@ -2640,3 +2641,40 @@ class TestVisionImageFormatContract: assert hasattr(AnthropicCompletion, "_convert_image_blocks"), ( "Anthropic provider must have _convert_image_blocks for auto-conversion" ) + + +class TestEnsureForceFinalAnswer: + """The forced final answer is requested with a trailing user turn.""" + + def test_forced_answer_request_ends_on_a_user_turn(self): + llm = Mock() + llm.call.return_value = "Final Answer: forced" + executor = _build_executor( + llm=llm, agent=SimpleNamespace(verbose=False), callbacks=[] + ) + executor.state.messages = [ + {"role": "user", "content": "Collect all the data."}, + {"role": "assistant", "content": "Thought: I need data\nObservation: partial"}, + ] + + result = AgentExecutor.ensure_force_final_answer(executor) + + assert result == "agent_finished" + sent = llm.call.call_args.args[0] + assert sent[-1] == { + "role": "user", + "content": I18N_DEFAULT.errors("force_final_answer"), + } + assert isinstance(executor.state.current_answer, AgentFinish) + assert executor.state.current_answer.output == "forced" + assert executor.state.is_finished is True + + def test_skips_the_llm_call_once_finished(self): + llm = Mock() + executor = _build_executor( + llm=llm, agent=SimpleNamespace(verbose=False), callbacks=[] + ) + executor.state.is_finished = True + + assert AgentExecutor.ensure_force_final_answer(executor) == "agent_finished" + llm.call.assert_not_called() diff --git a/lib/crewai/tests/agents/test_async_agent_executor.py b/lib/crewai/tests/agents/test_async_agent_executor.py index c0a5b4edd..517554bbb 100644 --- a/lib/crewai/tests/agents/test_async_agent_executor.py +++ b/lib/crewai/tests/agents/test_async_agent_executor.py @@ -10,9 +10,12 @@ from crewai.agent import Agent from crewai.agents.crew_agent_executor import CrewAgentExecutor from crewai.agents.parser import AgentAction, AgentFinish from crewai.agents.tools_handler import ToolsHandler +from crewai.llm import LLM from crewai.llms.base_llm import BaseLLM from crewai.task import Task +from crewai.tools import tool from crewai.tools.tool_types import ToolResult +from crewai.utilities.i18n import I18N_DEFAULT @pytest.fixture @@ -451,4 +454,90 @@ class TestAsyncLLMResponseHelper: messages=[{"role": "user", "content": "test"}], callbacks=[], printer=Printer(), - ) \ No newline at end of file + ) + + +class TestAsyncMaxIterationsForcedAnswer: + """Both async loops request the forced final answer with a trailing user turn.""" + + @pytest.mark.asyncio + async def test_react_loop_forces_final_answer_with_user_turn( + self, executor: CrewAgentExecutor, mock_llm: MagicMock + ) -> None: + mock_llm.call.return_value = "Final Answer: forced" + executor.iterations = executor.max_iter + executor.messages = [ + {"role": "user", "content": "Collect all the data."}, + {"role": "assistant", "content": "Thought: I need data\nObservation: partial"}, + ] + + with patch.object(executor, "_show_logs"): + result = await executor._ainvoke_loop() + + sent = mock_llm.call.call_args.args[0] + assert sent[-1] == { + "role": "user", + "content": I18N_DEFAULT.errors("force_final_answer"), + } + assert isinstance(result, AgentFinish) + assert result.output == "forced" + + @pytest.mark.asyncio + async def test_native_tools_loop_forces_final_answer_with_user_turn( + self, + test_agent: Agent, + test_task: Task, + mock_tools_handler: MagicMock, + ) -> None: + @tool + def get_data(step: str) -> str: + """Get data for a step.""" + return f"data for {step}" + + # BaseLLM has no supports_function_calling; the native loop needs it. + llm = MagicMock(spec=LLM) + llm.stop = [] + llm.supports_function_calling.return_value = True + llm.call.return_value = "Final Answer: forced" + executor = CrewAgentExecutor( + llm=llm, + task=test_task, + crew=None, + agent=test_agent, + prompt={"prompt": "Test prompt {input} {tool_names} {tools}"}, + max_iter=1, + tools=[], + original_tools=[get_data], + tools_names="get_data", + stop_words=[], + tools_description="", + tools_handler=mock_tools_handler, + ) + executor.iterations = 1 + executor.messages = [ + {"role": "user", "content": "Collect all the data."}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": "get_data", "arguments": "{}"}, + } + ], + }, + {"role": "tool", "tool_call_id": "call_1", "name": "get_data", "content": "partial"}, + {"role": "user", "content": I18N_DEFAULT.slice("post_tool_reasoning")}, + ] + + with patch.object(executor, "_show_logs"): + result = await executor._ainvoke_loop() + + sent = llm.call.call_args.args[0] + assert sent[-1] == { + "role": "user", + "content": I18N_DEFAULT.errors("force_final_answer"), + } + assert isinstance(result, AgentFinish) + assert result.output == "forced" diff --git a/lib/crewai/tests/agents/test_lite_agent.py b/lib/crewai/tests/agents/test_lite_agent.py index b19cc90a5..86c378279 100644 --- a/lib/crewai/tests/agents/test_lite_agent.py +++ b/lib/crewai/tests/agents/test_lite_agent.py @@ -1295,3 +1295,44 @@ class TestUsageMetricsDeltaSince: baseline = UsageMetrics(total_tokens=100, prompt_tokens=90, successful_requests=2) delta = UsageMetrics().delta_since(baseline) assert delta == UsageMetrics() + + +@pytest.mark.filterwarnings("ignore:LiteAgent is deprecated") +def test_lite_agent_forces_final_answer_with_user_turn(): + """The forced final answer is requested with a trailing user turn, not assistant prefill.""" + from crewai.utilities.i18n import I18N_DEFAULT + + requests: list[list[dict]] = [] + + def record_request(messages, **_kwargs): + # Snapshot: the agent keeps appending to this same list after the call. + requests.append([dict(message) for message in messages]) + return "Final Answer: forced" + + mock_llm = Mock(spec=LLM) + mock_llm.call.side_effect = record_request + mock_llm.stop = [] + mock_llm.get_token_usage_summary.return_value = UsageMetrics( + total_tokens=10, + prompt_tokens=5, + completion_tokens=5, + cached_prompt_tokens=0, + successful_requests=1, + ) + agent = LiteAgent( + role="Test Agent", + goal="Test goal", + backstory="Test backstory", + llm=mock_llm, + max_iterations=0, + verbose=False, + ) + + result = agent.kickoff("Collect all the data.") + + assert mock_llm.call.call_count == 1 + assert requests[0][-1] == { + "role": "user", + "content": I18N_DEFAULT.errors("force_final_answer"), + } + assert result.raw == "forced" diff --git a/lib/crewai/tests/llms/anthropic/test_anthropic.py b/lib/crewai/tests/llms/anthropic/test_anthropic.py index 22b4bbaf7..1308fdad0 100644 --- a/lib/crewai/tests/llms/anthropic/test_anthropic.py +++ b/lib/crewai/tests/llms/anthropic/test_anthropic.py @@ -1955,3 +1955,72 @@ def test_tool_fallback_still_used_for_models_without_native_support(): assert kwargs["tool_choice"] == {"type": "tool", "name": "structured_output"} assert "betas" not in kwargs mock_client.beta.messages.create.assert_not_called() + + +def _final_answer_response(text: str): + from anthropic.types import TextBlock + + mock_response = MagicMock() + mock_response.content = [TextBlock(type="text", text=text, citations=None)] + mock_response.usage = MagicMock(input_tokens=10, output_tokens=5) + mock_response.stop_reason = "end_turn" + mock_response.id = "msg_forced_answer" + return mock_response + + +@pytest.mark.parametrize( + "history_tail", + [ + [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "toolu_1", + "type": "function", + "function": {"name": "get_data", "arguments": "{}"}, + } + ], + }, + {"role": "tool", "tool_call_id": "toolu_1", "name": "get_data", "content": "partial"}, + {"role": "user", "content": "Analyze the tool result."}, + ], + [ + { + "role": "assistant", + "content": "Thought: I need data\nAction: get_data\nAction Input: {}\nObservation: partial", + }, + ], + ], + ids=["native-tools", "react"], +) +def test_max_iterations_request_ends_on_a_user_turn(history_tail): + """Claude 4.6+ rejects a request whose last message is an assistant turn (no prefill). + + Regression for the max_iter path: the forced final-answer instruction must + reach the Anthropic API as the trailing user message, on both loop shapes. + """ + from crewai.llms.providers.anthropic.completion import AnthropicCompletion + from crewai.utilities.agent_utils import handle_max_iterations_exceeded + from crewai.utilities.i18n import I18N_DEFAULT + + llm = AnthropicCompletion(model="claude-opus-5") + mock_client = MagicMock() + mock_client.messages.create.return_value = _final_answer_response("Final Answer: 42") + llm._client = mock_client + + history = [ + {"role": "system", "content": "You are an agent."}, + {"role": "user", "content": "Collect all the data."}, + *history_tail, + ] + + result = handle_max_iterations_exceeded( + printer=MagicMock(), messages=history, llm=llm, callbacks=[], verbose=False + ) + + mock_client.messages.create.assert_called_once() + sent = mock_client.messages.create.call_args.kwargs["messages"] + assert sent[-1] == {"role": "user", "content": I18N_DEFAULT.errors("force_final_answer")} + assert result.output == "42" diff --git a/lib/crewai/tests/utilities/test_agent_utils.py b/lib/crewai/tests/utilities/test_agent_utils.py index 58dc28817..a806eef75 100644 --- a/lib/crewai/tests/utilities/test_agent_utils.py +++ b/lib/crewai/tests/utilities/test_agent_utils.py @@ -16,6 +16,7 @@ from crewai.hooks.tool_hooks import ( clear_before_tool_call_hooks, register_after_tool_call_hook, ) +from crewai.agents.parser import AgentFinish from crewai.tools.base_tool import BaseTool from crewai.llm import CONTEXT_WINDOW_USAGE_RATIO from crewai.utilities.agent_utils import ( @@ -30,6 +31,7 @@ from crewai.utilities.agent_utils import ( _split_text_by_token_limit, format_message_for_llm, convert_tools_to_openai_schema, + handle_max_iterations_exceeded, execute_single_native_tool_call, extract_tool_call_info, is_tool_call_list, @@ -1652,3 +1654,107 @@ class TestResolvePlusResponse: resolve_plus_response(future) asyncio.run(main()) + + +_FORCE_FINAL_ANSWER = I18N_DEFAULT.errors("force_final_answer") + + +def _native_tool_history() -> list[dict[str, Any]]: + """History as the native tool-calling loop leaves it: ends on a user prompt.""" + return [ + {"role": "system", "content": "You are an agent."}, + {"role": "user", "content": "Collect all the data."}, + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call_1", + "type": "function", + "function": {"name": "get_data", "arguments": "{}"}, + } + ], + }, + {"role": "tool", "tool_call_id": "call_1", "name": "get_data", "content": "partial"}, + {"role": "user", "content": I18N_DEFAULT.slice("post_tool_reasoning")}, + ] + + +def _react_history() -> list[dict[str, Any]]: + """History as the ReAct loop leaves it: ends on the assistant turn with the observation.""" + return [ + {"role": "system", "content": "You are an agent."}, + {"role": "user", "content": "Collect all the data."}, + { + "role": "assistant", + "content": "Thought: I need data\nAction: get_data\nAction Input: {}\nObservation: partial", + }, + ] + + +class TestHandleMaxIterationsExceeded: + """The forced final answer is requested with a user turn, never assistant prefill. + + Current Claude models reject a request whose last message is an assistant + turn ("This model does not support assistant message prefill"), so the + nudge must go out as the user's instruction on every loop shape. + """ + + @pytest.mark.parametrize( + "make_history", [_native_tool_history, _react_history], ids=["native-tools", "react"] + ) + def test_appends_the_instruction_as_a_user_turn(self, make_history) -> None: + history = make_history() + before = [dict(message) for message in history] + llm = MagicMock() + llm.call.return_value = "Final Answer: 42" + + result = handle_max_iterations_exceeded( + printer=MagicMock(), messages=history, llm=llm, callbacks=[], verbose=False + ) + + assert history[:-1] == before + assert history[-1] == {"role": "user", "content": _FORCE_FINAL_ANSWER} + llm.call.assert_called_once_with(history, callbacks=[]) + assert isinstance(result, AgentFinish) + assert result.output == "42" + + def test_action_shaped_reply_still_becomes_a_final_answer(self) -> None: + reply = "Thought: one more\nAction: get_data\nAction Input: {}" + llm = MagicMock() + llm.call.return_value = reply + + result = handle_max_iterations_exceeded( + printer=MagicMock(), messages=_react_history(), llm=llm, callbacks=[], verbose=False + ) + + assert isinstance(result, AgentFinish) + assert result.text == reply + assert result.output == reply + + @pytest.mark.parametrize("reply", [None, ""], ids=["none", "empty"]) + def test_empty_reply_raises(self, reply: str | None) -> None: + llm = MagicMock() + llm.call.return_value = reply + + with pytest.raises(ValueError, match="Invalid response from LLM call - None or empty."): + handle_max_iterations_exceeded( + printer=MagicMock(), messages=_native_tool_history(), llm=llm, callbacks=[], verbose=False + ) + + @pytest.mark.parametrize("verbose", [True, False]) + def test_notice_is_printed_only_when_verbose(self, verbose: bool) -> None: + printer = MagicMock() + llm = MagicMock() + llm.call.return_value = "Final Answer: 42" + + handle_max_iterations_exceeded( + printer=printer, messages=_native_tool_history(), llm=llm, callbacks=[], verbose=verbose + ) + + if verbose: + printer.print.assert_called_once_with( + content="Maximum iterations reached. Requesting final answer.", color="yellow" + ) + else: + printer.print.assert_not_called() From 7e80d94921ddbbcafb9b0654000f2d74da47a8e4 Mon Sep 17 00:00:00 2001 From: Modusensus Date: Mon, 14 Sep 2026 17:08:50 +0800 Subject: [PATCH 10/11] fix: use sys.platform guards so mypy passes on Windows (#7401) mypy does not narrow on `platform.system()`, so Windows-based contributors get 8 spurious attr-defined/unused-ignore errors from the termios and resource imports. Switch to `sys.platform` comparisons, which mypy understands natively, and drop the now-unneeded type-ignore comments. Fixes #7400 Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> --- .../src/crewai/memory/storage/lancedb_storage.py | 16 +++++++++------- lib/crewai/src/crewai/utilities/crew_chat.py | 7 +++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py index 8c9d640a5..6a97900fd 100644 --- a/lib/crewai/src/crewai/memory/storage/lancedb_storage.py +++ b/lib/crewai/src/crewai/memory/storage/lancedb_storage.py @@ -8,6 +8,7 @@ import json import logging import os from pathlib import Path +import sys import threading import time from typing import Any @@ -77,14 +78,15 @@ class LanceDBStorage: self._table_name = table_name self._db = lancedb.connect(str(self._path)) - try: - import resource + if sys.platform != "win32": + try: + import resource - soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) - if soft < 4096: - resource.setrlimit(resource.RLIMIT_NOFILE, (min(hard, 4096), hard)) - except Exception: # noqa: S110 - pass # Windows or already at the max hard limit — safe to ignore + soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE) + if soft < 4096: + resource.setrlimit(resource.RLIMIT_NOFILE, (min(hard, 4096), hard)) + except Exception: # noqa: S110 + pass # Already at the max hard limit — safe to ignore self._compact_every = compact_every self._save_count = 0 diff --git a/lib/crewai/src/crewai/utilities/crew_chat.py b/lib/crewai/src/crewai/utilities/crew_chat.py index 8fa5a685c..50d7ebc20 100644 --- a/lib/crewai/src/crewai/utilities/crew_chat.py +++ b/lib/crewai/src/crewai/utilities/crew_chat.py @@ -3,7 +3,6 @@ import contextvars import json from pathlib import Path -import platform import re import sys import threading @@ -175,11 +174,11 @@ def create_tool_function(crew: Crew, messages: list[LLMMessage]) -> Any: def flush_input() -> None: """Flush any pending input from the user.""" - if platform.system() == "Windows": + if sys.platform == "win32": import msvcrt - while msvcrt.kbhit(): # type: ignore[attr-defined] - msvcrt.getch() # type: ignore[attr-defined] + while msvcrt.kbhit(): + msvcrt.getch() else: import termios From a3287100077e33751fc6843c5a701808a2f31266 Mon Sep 17 00:00:00 2001 From: Shivangi Date: Mon, 14 Sep 2026 15:52:31 +0530 Subject: [PATCH 11/11] fix: reject replay when stored tasks differ (#7155) * fix: reject replay when stored tasks differ * fix: validate replay task descriptions * fix: reject ambiguous replay task identities * fix: persist stable replay task keys * test: align legacy replay fixture --------- Co-authored-by: Vidit Ostwal <110953813+Vidit-Ostwal@users.noreply.github.com> --- lib/crewai/src/crewai/crew.py | 59 +++++ .../storage/kickoff_task_outputs_storage.py | 35 ++- .../utilities/task_output_storage_handler.py | 1 + lib/crewai/tests/test_crew.py | 208 ++++++++++++++++-- 4 files changed, 277 insertions(+), 26 deletions(-) diff --git a/lib/crewai/src/crewai/crew.py b/lib/crewai/src/crewai/crew.py index 0f77b2d22..6f20044b5 100644 --- a/lib/crewai/src/crewai/crew.py +++ b/lib/crewai/src/crewai/crew.py @@ -2031,6 +2031,63 @@ class Crew(FlowTrackable, BaseModel): None, ) + def _validate_replay_tasks( + self, stored_outputs: list[Any], start_index: int + ) -> None: + """Ensure stored outputs still correspond to the tasks that will receive them.""" + if len(self.tasks) <= start_index: + raise ValueError( + "Cannot replay because the current crew does not match the stored task outputs." + ) + + stored_prefix = stored_outputs[: start_index + 1] + stored_task_keys = [ + stored_output.get("task_key") for stored_output in stored_prefix + ] + current_task_keys = [task.key for task in self.tasks[: start_index + 1]] + if all(stored_task_keys): + if len(set(stored_task_keys)) != len(stored_task_keys) or len( + set(current_task_keys) + ) != len(current_task_keys): + raise ValueError( + "Cannot replay because the stored task identities are ambiguous." + ) + if stored_task_keys != current_task_keys: + raise ValueError( + "Cannot replay because the current crew does not match the stored task outputs." + ) + return + + stored_identities = [ + ( + stored_output["output"].get("description"), + stored_output.get("expected_output"), + ) + for stored_output in stored_prefix + ] + current_identities = [ + (task.description, task.expected_output) + for task in self.tasks[: start_index + 1] + ] + if len(set(stored_identities)) != len(stored_identities) or len( + set(current_identities) + ) != len(current_identities): + raise ValueError( + "Cannot replay because the stored task identities are ambiguous." + ) + + for index, stored_output in enumerate(stored_prefix): + task = self.tasks[index] + output = stored_output["output"] + stored_expected_output = stored_output.get("expected_output") + if task.description != output.get("description") or ( + stored_expected_output is not None + and task.expected_output != stored_expected_output + ): + raise ValueError( + "Cannot replay because the current crew does not match the stored task outputs." + ) + def replay(self, task_id: str, inputs: dict[str, Any] | None = None) -> CrewOutput: """Replay the crew execution from a specific task.""" stored_outputs = self._task_output_handler.load() @@ -2042,6 +2099,8 @@ class Crew(FlowTrackable, BaseModel): if start_index is None: raise ValueError(f"Task with id {task_id} not found in the crew's tasks.") + self._validate_replay_tasks(stored_outputs, start_index) + replay_inputs = ( inputs if inputs is not None else stored_outputs[start_index]["inputs"] ) diff --git a/lib/crewai/src/crewai/memory/storage/kickoff_task_outputs_storage.py b/lib/crewai/src/crewai/memory/storage/kickoff_task_outputs_storage.py index 0ff58cc9c..800c4b2df 100644 --- a/lib/crewai/src/crewai/memory/storage/kickoff_task_outputs_storage.py +++ b/lib/crewai/src/crewai/memory/storage/kickoff_task_outputs_storage.py @@ -32,8 +32,8 @@ class KickoffTaskOutputsSQLiteStorage: """Initialize the SQLite database and create the latest_kickoff_task_outputs table. This method sets up the database schema for storing task outputs. It creates - a table with columns for task_id, expected_output, output (as JSON), - task_index, inputs (as JSON), was_replayed flag, and timestamp. + a table with columns for task_id, task_key, expected_output, output (as + JSON), task_index, inputs (as JSON), was_replayed flag, and timestamp. Raises: DatabaseOperationError: If database initialization fails due to SQLite errors. @@ -47,6 +47,7 @@ class KickoffTaskOutputsSQLiteStorage: """ CREATE TABLE IF NOT EXISTS latest_kickoff_task_outputs ( task_id TEXT PRIMARY KEY, + task_key TEXT, expected_output TEXT, output JSON, task_index INTEGER, @@ -56,6 +57,16 @@ class KickoffTaskOutputsSQLiteStorage: ) """ ) + columns = { + row[1] + for row in cursor.execute( + "PRAGMA table_info(latest_kickoff_task_outputs)" + ) + } + if "task_key" not in columns: + cursor.execute( + "ALTER TABLE latest_kickoff_task_outputs ADD COLUMN task_key TEXT" + ) conn.commit() except sqlite3.Error as e: @@ -92,11 +103,12 @@ class KickoffTaskOutputsSQLiteStorage: cursor.execute( """ INSERT OR REPLACE INTO latest_kickoff_task_outputs - (task_id, expected_output, output, task_index, inputs, was_replayed) - VALUES (?, ?, ?, ?, ?, ?) + (task_id, task_key, expected_output, output, task_index, inputs, was_replayed) + VALUES (?, ?, ?, ?, ?, ?, ?) """, ( str(task.id), + task.key, task.expected_output, json.dumps(output, cls=CrewJSONEncoder), task_index, @@ -174,7 +186,7 @@ class KickoffTaskOutputsSQLiteStorage: with sqlite3.connect(self.db_path, timeout=30) as conn: cursor = conn.cursor() cursor.execute(""" - SELECT * + SELECT task_id, task_key, expected_output, output, task_index, inputs, was_replayed, timestamp FROM latest_kickoff_task_outputs ORDER BY task_index """) @@ -184,12 +196,13 @@ class KickoffTaskOutputsSQLiteStorage: for row in rows: result = { "task_id": row[0], - "expected_output": row[1], - "output": json.loads(row[2]), - "task_index": row[3], - "inputs": json.loads(row[4]), - "was_replayed": row[5], - "timestamp": row[6], + "task_key": row[1], + "expected_output": row[2], + "output": json.loads(row[3]), + "task_index": row[4], + "inputs": json.loads(row[5]), + "was_replayed": row[6], + "timestamp": row[7], } results.append(result) diff --git a/lib/crewai/src/crewai/utilities/task_output_storage_handler.py b/lib/crewai/src/crewai/utilities/task_output_storage_handler.py index 2259bb833..cfff66815 100644 --- a/lib/crewai/src/crewai/utilities/task_output_storage_handler.py +++ b/lib/crewai/src/crewai/utilities/task_output_storage_handler.py @@ -43,6 +43,7 @@ class TaskOutputStorageHandler: if log.get("was_replayed", False): replayed = { "task_id": str(log["task"].id), + "task_key": log["task"].key, "expected_output": log["task"].expected_output, "output": log["output"], "was_replayed": log["was_replayed"], diff --git a/lib/crewai/tests/test_crew.py b/lib/crewai/tests/test_crew.py index 0195112cb..a960a6b09 100644 --- a/lib/crewai/tests/test_crew.py +++ b/lib/crewai/tests/test_crew.py @@ -3045,16 +3045,38 @@ def test_replay_feature(researcher, writer): ) with patch.object(Task, "execute_sync") as mock_execute_task: - mock_execute_task.return_value = TaskOutput( - description="Mock description", - raw="Mocked output for list of ideas", - agent="Researcher", - json_dict=None, - output_format=OutputFormat.RAW, - pydantic=None, - summary="Mocked output for list of ideas", - messages=[], - ) + mock_execute_task.side_effect = [ + TaskOutput( + description=list_ideas.description, + raw="Mocked output for list of ideas", + agent="Researcher", + json_dict=None, + output_format=OutputFormat.RAW, + pydantic=None, + summary="Mocked output for list of ideas", + messages=[], + ), + TaskOutput( + description=write.description, + raw="Mocked output for list of ideas", + agent="Researcher", + json_dict=None, + output_format=OutputFormat.RAW, + pydantic=None, + summary="Mocked output for list of ideas", + messages=[], + ), + TaskOutput( + description=write.description, + raw="Mocked output for list of ideas", + agent="Researcher", + json_dict=None, + output_format=OutputFormat.RAW, + pydantic=None, + summary="Mocked output for list of ideas", + messages=[], + ), + ] crew.kickoff() crew.replay(str(write.id)) @@ -3062,6 +3084,154 @@ def test_replay_feature(researcher, writer): assert mock_execute_task.call_count == 3 +def test_replay_rejects_changed_task_order(researcher): + """Replay must not restore a saved output onto a different current task.""" + research = Task( + description="Research the topic", + expected_output="Research notes", + agent=researcher, + ) + write = Task( + description="Write the article", + expected_output="An article", + agent=researcher, + ) + plan = Task( + description="Plan the article", + expected_output="An outline", + agent=researcher, + ) + crew = Crew(agents=[researcher], tasks=[plan, research, write]) + + stored_outputs = [ + { + "task_id": str(research.id), + "expected_output": research.expected_output, + "output": {"description": research.description}, + "inputs": {}, + }, + { + "task_id": str(write.id), + "expected_output": write.expected_output, + "output": {"description": write.description}, + "inputs": {}, + }, + ] + with patch( + "crewai.utilities.task_output_storage_handler.TaskOutputStorageHandler.load", + return_value=stored_outputs, + ): + with pytest.raises(ValueError, match="current crew does not match"): + crew.replay(str(write.id)) + + +def test_replay_rejects_reordered_tasks_with_matching_expected_output(researcher): + """Task descriptions keep replay from confusing tasks with the same expected output.""" + research = Task( + description="Research the topic", + expected_output="A report", + agent=researcher, + ) + write = Task( + description="Write the article", + expected_output="A report", + agent=researcher, + ) + crew = Crew(agents=[researcher], tasks=[write, research]) + + stored_outputs = [ + { + "task_id": str(research.id), + "expected_output": research.expected_output, + "output": {"description": research.description}, + "inputs": {}, + }, + { + "task_id": str(write.id), + "expected_output": write.expected_output, + "output": {"description": write.description}, + "inputs": {}, + }, + ] + with patch( + "crewai.utilities.task_output_storage_handler.TaskOutputStorageHandler.load", + return_value=stored_outputs, + ): + with pytest.raises(ValueError, match="current crew does not match"): + crew.replay(str(write.id)) + + +def test_replay_rejects_ambiguous_task_identities(researcher): + """Replay must fail loud when persisted task details cannot identify a task.""" + first_task = Task( + description="Write a report", + expected_output="A report", + agent=researcher, + ) + second_task = Task( + description="Write a report", + expected_output="A report", + agent=researcher, + ) + crew = Crew(agents=[researcher], tasks=[second_task, first_task]) + + stored_outputs = [ + { + "task_id": str(first_task.id), + "expected_output": first_task.expected_output, + "output": {"description": first_task.description}, + "inputs": {}, + }, + { + "task_id": str(second_task.id), + "expected_output": second_task.expected_output, + "output": {"description": second_task.description}, + "inputs": {}, + }, + ] + with patch( + "crewai.utilities.task_output_storage_handler.TaskOutputStorageHandler.load", + return_value=stored_outputs, + ): + with pytest.raises(ValueError, match="task identities are ambiguous"): + crew.replay(str(second_task.id)) + + +def test_replay_uses_task_key_before_interpolating_new_inputs(researcher): + """A replayed template remains identifiable when the new inputs differ.""" + task = Task( + description="Say hello to {name}", + expected_output="A greeting for {name}", + agent=researcher, + ) + task.interpolate_inputs_and_add_conversation_history({"name": "John"}) + stored_output = { + "task_id": str(task.id), + "task_key": task.key, + "expected_output": task.expected_output, + "output": {"description": task.description}, + "inputs": {"name": "John"}, + } + replay_task = Task( + description="Say hello to {name}", + expected_output="A greeting for {name}", + agent=researcher, + ) + crew = Crew(agents=[researcher], tasks=[replay_task]) + + with ( + patch( + "crewai.utilities.task_output_storage_handler.TaskOutputStorageHandler.load", + return_value=[stored_output], + ), + patch.object(crew, "_execute_tasks"), + ): + crew.replay(str(task.id), inputs={"name": "Maria"}) + + assert replay_task.description == "Say hello to Maria" + assert replay_task.expected_output == "A greeting for Maria" + + @pytest.mark.vcr() def test_crew_replay_error(researcher, writer): task = Task( @@ -3292,7 +3462,7 @@ def test_replay_with_context(): ) context_output = TaskOutput( - description="Context Task Output", + description=task1.description, agent="test_agent", raw="context raw output", pydantic=None, @@ -3309,6 +3479,7 @@ def test_replay_with_context(): return_value=[ { "task_id": str(task1.id), + "expected_output": task1.expected_output, "output": { "description": context_output.description, "summary": context_output.summary, @@ -3322,8 +3493,9 @@ def test_replay_with_context(): }, { "task_id": str(task2.id), + "expected_output": task2.expected_output, "output": { - "description": "Test Task Output", + "description": task2.description, "summary": None, "raw": "test raw output", "pydantic": None, @@ -3394,6 +3566,7 @@ def test_replay_with_invalid_task_id(): return_value=[ { "task_id": str(task1.id), + "task_key": task1.key, "output": { "description": context_output.description, "summary": context_output.summary, @@ -3407,6 +3580,7 @@ def test_replay_with_invalid_task_id(): }, { "task_id": str(task2.id), + "task_key": task2.key, "output": { "description": "Test Task Output", "summary": None, @@ -3460,6 +3634,7 @@ def test_replay_interpolates_inputs_properly(mock_interpolate_inputs): return_value=[ { "task_id": str(task1.id), + "task_key": task1.key, "output": { "description": context_output.description, "summary": context_output.summary, @@ -3473,6 +3648,7 @@ def test_replay_interpolates_inputs_properly(mock_interpolate_inputs): }, { "task_id": str(task2.id), + "task_key": task2.key, "output": { "description": "Test Task Output", "summary": None, @@ -3501,7 +3677,7 @@ def test_replay_setup_context(): agent=agent, ) context_output = TaskOutput( - description="Context Task Output", + description=task1.description, agent="test_agent", raw="context raw output", pydantic=None, @@ -3525,12 +3701,13 @@ def test_replay_setup_context(): "output_format": context_output.output_format, "agent": context_output.agent, }, + "expected_output": task1.expected_output, "inputs": {"name": "John"}, }, { "task_id": str(task2.id), "output": { - "description": "Test Task Output", + "description": task2.description, "summary": None, "raw": "test raw output", "pydantic": None, @@ -3538,6 +3715,7 @@ def test_replay_setup_context(): "output_format": "json", "agent": "test_agent", }, + "expected_output": task2.expected_output, "inputs": {"name": "John"}, }, ], @@ -3546,7 +3724,7 @@ def test_replay_setup_context(): assert crew.tasks[0].output is not None assert isinstance(crew.tasks[0].output, TaskOutput) - assert crew.tasks[0].output.description == "Context Task Output" + assert crew.tasks[0].output.description == task1.description assert crew.tasks[0].output.agent == "test_agent" assert crew.tasks[0].output.raw == "context raw output" assert crew.tasks[0].output.output_format == OutputFormat.RAW