From 21678f8ac6254dfb1b4c9b3ab37882263bb2219e Mon Sep 17 00:00:00 2001 From: Melanie Hart Buehler Date: Sun, 13 Sep 2026 22:51:43 -0700 Subject: [PATCH 1/2] 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 2/2] 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()