From 6c19669d630f7bb3862ce3b1d9551aa497069fed Mon Sep 17 00:00:00 2001 From: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> Date: Tue, 11 Aug 2026 10:36:47 -0700 Subject: [PATCH] refactor: update date injection functionality in agents (#6850) * refactor: update date injection functionality in agents - Changed the description of the parameter to clarify that it injects the current date into the agent's prompt instead of tasks. - Removed the method as it was no longer needed. - Implemented a new method in the class to handle date injection directly into the prompt. - Updated tests to ensure the date is correctly injected into the system prompt and user messages based on the flag. * translations * nit --- docs/edge/ar/concepts/agents.mdx | 4 +- docs/edge/en/concepts/agents.mdx | 6 +- docs/edge/ko/concepts/agents.mdx | 6 +- docs/edge/pt-BR/concepts/agents.mdx | 6 +- lib/crewai/src/crewai/agent/core.py | 33 +-- lib/crewai/src/crewai/utilities/prompts.py | 48 +++- .../tests/agents/test_agent_inject_date.py | 223 +++++++++++------- 7 files changed, 195 insertions(+), 131 deletions(-) diff --git a/docs/edge/ar/concepts/agents.mdx b/docs/edge/ar/concepts/agents.mdx index 685320db2d..5e919de949 100644 --- a/docs/edge/ar/concepts/agents.mdx +++ b/docs/edge/ar/concepts/agents.mdx @@ -60,7 +60,7 @@ mode: "wide" | **احترام نافذة السياق** _(اختياري)_ | `respect_context_window` | `bool` | إبقاء الرسائل تحت حجم نافذة السياق عبر التلخيص. الافتراضي True. | | **وضع تنفيذ الكود** _(اختياري)_ | `code_execution_mode` | `Literal["safe", "unsafe"]` | وضع تنفيذ الكود: 'safe' (باستخدام Docker) أو 'unsafe' (مباشر). الافتراضي 'safe'. | | **متعدد الوسائط** _(اختياري)_ | `multimodal` | `bool` | ما إذا كان الوكيل يدعم القدرات متعددة الوسائط. الافتراضي False. | -| **حقن التاريخ** _(اختياري)_ | `inject_date` | `bool` | ما إذا كان يتم حقن التاريخ الحالي تلقائيًا في المهام. الافتراضي False. | +| **حقن التاريخ** _(اختياري)_ | `inject_date` | `bool` | ما إذا كان يتم حقن التاريخ الحالي تلقائيًا في أمر الوكيل. الافتراضي False. | | **تنسيق التاريخ** _(اختياري)_ | `date_format` | `str` | سلسلة تنسيق التاريخ عند تفعيل inject_date. الافتراضي "%Y-%m-%d" (تنسيق ISO). | | **الاستدلال** _(اختياري)_ | `reasoning` | `bool` | ما إذا كان يجب على الوكيل التأمل وإنشاء خطة قبل تنفيذ المهمة. الافتراضي False. | | **الحد الأقصى لمحاولات الاستدلال** _(اختياري)_ | `max_reasoning_attempts` | `Optional[int]` | الحد الأقصى لمحاولات الاستدلال قبل تنفيذ المهمة. إذا None، سيحاول حتى الاستعداد. | @@ -287,7 +287,7 @@ analysis_agent = Agent( - `multimodal`: تفعيل القدرات متعددة الوسائط لمعالجة النص والمحتوى المرئي - `reasoning`: تمكين الوكيل من التأمل وإنشاء خطط قبل تنفيذ المهام -- `inject_date`: حقن التاريخ الحالي تلقائيًا في أوصاف المهام +- `inject_date`: حقن التاريخ الحالي تلقائيًا في أمر الوكيل #### القوالب diff --git a/docs/edge/en/concepts/agents.mdx b/docs/edge/en/concepts/agents.mdx index 98fffbf6e6..bee0fdcd8d 100644 --- a/docs/edge/en/concepts/agents.mdx +++ b/docs/edge/en/concepts/agents.mdx @@ -61,7 +61,7 @@ The Visual Agent Builder enables: | **Respect Context Window** _(optional)_ | `respect_context_window` | `bool` | Keep messages under context window size by summarizing. Default is True. | | **Code Execution Mode** _(optional)_ | `code_execution_mode` | `Literal["safe", "unsafe"]` | Mode for code execution: 'safe' (using Docker) or 'unsafe' (direct). Default is 'safe'. | | **Multimodal** _(optional)_ | `multimodal` | `bool` | Whether the agent supports multimodal capabilities. Default is False. | -| **Inject Date** _(optional)_ | `inject_date` | `bool` | Whether to automatically inject the current date into tasks. Default is False. | +| **Inject Date** _(optional)_ | `inject_date` | `bool` | Whether to automatically inject the current date into the agent's prompt. Default is False. | | **Date Format** _(optional)_ | `date_format` | `str` | Format string for date when inject_date is enabled. Default is "%Y-%m-%d" (ISO format). | | **Reasoning** _(optional)_ | `reasoning` | `bool` | Whether the agent should reflect and create a plan before executing a task. Default is False. | | **Max Reasoning Attempts** _(optional)_ | `max_reasoning_attempts` | `Optional[int]` | Maximum number of reasoning attempts before executing the task. If None, will try until ready. | @@ -236,7 +236,7 @@ strategic_agent = Agent( role="Market Analyst", goal="Track market movements with precise date references and strategic planning", backstory="Expert in time-sensitive financial analysis and strategic reporting", - inject_date=True, # Automatically inject current date into tasks + inject_date=True, # Automatically inject current date into the prompt date_format="%B %d, %Y", # Format as "May 21, 2025" reasoning=True, # Enable strategic planning max_reasoning_attempts=2, # Limit planning iterations @@ -303,7 +303,7 @@ multimodal_agent = Agent( - `multimodal`: Enable multimodal capabilities for processing text and visual content - `reasoning`: Enable agent to reflect and create plans before executing tasks -- `inject_date`: Automatically inject current date into task descriptions +- `inject_date`: Automatically inject current date into the agents prompt #### Templates diff --git a/docs/edge/ko/concepts/agents.mdx b/docs/edge/ko/concepts/agents.mdx index f5cfb93d34..b78fab1091 100644 --- a/docs/edge/ko/concepts/agents.mdx +++ b/docs/edge/ko/concepts/agents.mdx @@ -56,7 +56,7 @@ CrewAI AOP에는 코드를 작성하지 않고도 에이전트 생성 및 구성 | **컨텍스트 윈도우 준수** _(옵션)_ | `respect_context_window` | `bool` | 메시지를 컨텍스트 윈도우 크기 내로 유지하기 위하여 요약 기능을 사용합니다. 기본값은 True입니다. | | **코드 실행 모드** _(옵션)_ | `code_execution_mode` | `Literal["safe", "unsafe"]` | 코드 실행 모드: 'safe'(Docker 사용) 또는 'unsafe'(직접 실행). 기본값은 'safe'입니다. | | **멀티모달** _(옵션)_ | `multimodal` | `bool` | 에이전트가 멀티모달 기능을 지원하는지 여부입니다. 기본값은 False입니다. | -| **날짜 자동 삽입** _(옵션)_ | `inject_date` | `bool` | 작업에 현재 날짜를 자동으로 삽입할지 여부입니다. 기본값은 False입니다. | +| **날짜 자동 삽입** _(옵션)_ | `inject_date` | `bool` | 에이전트 프롬프트에 현재 날짜를 자동으로 삽입할지 여부입니다. 기본값은 False입니다. | | **날짜 형식** _(옵션)_ | `date_format` | `str` | inject_date 활성화 시 날짜 표시 형식 문자열입니다. 기본값은 "%Y-%m-%d"(ISO 포맷)입니다. | | **추론** _(옵션)_ | `reasoning` | `bool` | 에이전트가 작업을 실행하기 전에 반영 및 플랜을 생성할지 여부입니다. 기본값은 False입니다. | | **최대 추론 시도 수** _(옵션)_ | `max_reasoning_attempts` | `Optional[int]` | 작업 실행 전 최대 추론 시도 횟수입니다. 설정하지 않으면 준비될 때까지 시도합니다. | @@ -267,7 +267,7 @@ strategic_agent = Agent( role="Market Analyst", goal="Track market movements with precise date references and strategic planning", backstory="Expert in time-sensitive financial analysis and strategic reporting", - inject_date=True, # Automatically inject current date into tasks + inject_date=True, # Automatically inject current date into the prompt date_format="%B %d, %Y", # Format as "May 21, 2025" reasoning=True, # Enable strategic planning max_reasoning_attempts=2, # Limit planning iterations @@ -328,7 +328,7 @@ multimodal_agent = Agent( #### 고급 기능 - `multimodal`: 텍스트와 시각적 콘텐츠 처리를 위한 멀티모달 기능 활성화 - `reasoning`: 에이전트가 작업을 수행하기 전에 반영하고 계획을 작성할 수 있도록 활성화 -- `inject_date`: 현재 날짜를 작업 설명에 자동으로 삽입 +- `inject_date`: 현재 날짜를 에이전트 프롬프트에 자동으로 삽입 #### 템플릿 - `system_template`: 에이전트의 핵심 동작을 정의합니다 diff --git a/docs/edge/pt-BR/concepts/agents.mdx b/docs/edge/pt-BR/concepts/agents.mdx index 53312348d5..7c9f1ce5fc 100644 --- a/docs/edge/pt-BR/concepts/agents.mdx +++ b/docs/edge/pt-BR/concepts/agents.mdx @@ -61,7 +61,7 @@ O Construtor Visual de Agentes permite: | **Respect Context Window** _(opcional)_ | `respect_context_window` | `bool` | Mantém as mensagens dentro do tamanho da janela de contexto, resumindo quando necessário. Padrão: True. | | **Code Execution Mode** _(opcional)_ | `code_execution_mode` | `Literal["safe", "unsafe"]` | Modo de execução de código: 'safe' (usando Docker) ou 'unsafe' (direto). Padrão: 'safe'. | | **Multimodal** _(opcional)_ | `multimodal` | `bool` | Se o agente suporta capacidades multimodais. Padrão: False. | -| **Inject Date** _(opcional)_ | `inject_date` | `bool` | Se deve injetar automaticamente a data atual nas tarefas. Padrão: False. | +| **Inject Date** _(opcional)_ | `inject_date` | `bool` | Se deve injetar automaticamente a data atual no prompt do agente. Padrão: False. | | **Date Format** _(opcional)_ | `date_format` | `str` | Formato de data utilizado quando `inject_date` está ativo. Padrão: "%Y-%m-%d" (formato ISO). | | **Reasoning** _(opcional)_ | `reasoning` | `bool` | Se o agente deve refletir e criar um plano antes de executar uma tarefa. Padrão: False. | | **Max Reasoning Attempts** _(opcional)_ | `max_reasoning_attempts` | `Optional[int]` | Número máximo de tentativas de raciocínio antes de executar a tarefa. Se None, tentará até estar pronto. | @@ -274,7 +274,7 @@ strategic_agent = Agent( role="Analista de Mercado", goal="Acompanhar movimentos do mercado com referências de datas precisas e planejamento estratégico", backstory="Especialista em análise financeira sensível ao tempo e relatórios estratégicos", - inject_date=True, # Injeta automaticamente a data atual nas tarefas + inject_date=True, # Injeta automaticamente a data atual no prompt date_format="%d de %B de %Y", # Exemplo: "21 de maio de 2025" reasoning=True, # Ativa planejamento estratégico max_reasoning_attempts=2, # Limite de iterações de planejamento @@ -341,7 +341,7 @@ multimodal_agent = Agent( - `multimodal`: Habilita capacidades multimodais para processar texto e conteúdo visual - `reasoning`: Permite que o agente reflita e crie planos antes de executar tarefas -- `inject_date`: Injeta a data atual automaticamente nas descrições das tarefas +- `inject_date`: Injeta a data atual automaticamente no prompt do agente #### Templates diff --git a/lib/crewai/src/crewai/agent/core.py b/lib/crewai/src/crewai/agent/core.py index ac98c20f4c..70e090c6f4 100644 --- a/lib/crewai/src/crewai/agent/core.py +++ b/lib/crewai/src/crewai/agent/core.py @@ -6,7 +6,6 @@ import asyncio from collections.abc import Callable, Coroutine, Sequence import concurrent.futures import contextvars -from datetime import datetime import inspect import json import os @@ -264,7 +263,7 @@ class Agent(BaseAgent): ) inject_date: bool = Field( default=False, - description="Whether to automatically inject the current date into tasks.", + description="Whether to automatically inject the current date into the agent's prompt.", ) date_format: str = Field( default="%Y-%m-%d", @@ -545,7 +544,7 @@ class Agent(BaseAgent): ) -> str: """Prepare common setup for task execution shared by sync and async paths. - Handles reasoning, date injection, prompt building, and memory retrieval. + Handles reasoning, prompt building, and memory retrieval. Args: task: Task to execute. @@ -556,8 +555,6 @@ class Agent(BaseAgent): """ get_env_context() - self._inject_date_to_task(task) - self.reset_tool_failures() if self.tools_handler: @@ -1332,32 +1329,6 @@ class Agent(BaseAgent): ] ) - def _inject_date_to_task(self, task: Task) -> None: - """Inject the current date into the task description if inject_date is enabled.""" - if self.inject_date: - try: - valid_format_codes = [ - "%Y", - "%m", - "%d", - "%H", - "%M", - "%S", - "%B", - "%b", - "%A", - "%a", - ] - is_valid = any(code in self.date_format for code in valid_format_codes) - - if not is_valid: - raise ValueError(f"Invalid date format: {self.date_format}") - - current_date = datetime.now().strftime(self.date_format) - task.description += f"\n\nCurrent Date: {current_date}" - except Exception as e: - self._logger.log("warning", f"Failed to inject date: {e!s}") - def _validate_docker_installation(self) -> None: """Deprecated: No-op. CodeInterpreterTool is no longer available.""" warnings.warn( diff --git a/lib/crewai/src/crewai/utilities/prompts.py b/lib/crewai/src/crewai/utilities/prompts.py index 78d59138d2..c379b91810 100644 --- a/lib/crewai/src/crewai/utilities/prompts.py +++ b/lib/crewai/src/crewai/utilities/prompts.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import datetime from typing import Any, Literal from pydantic import BaseModel, Field @@ -9,6 +10,20 @@ from pydantic import BaseModel, Field from crewai.utilities.i18n import I18N_DEFAULT +VALID_DATE_FORMAT_CODES = ( + "%Y", + "%m", + "%d", + "%H", + "%M", + "%S", + "%B", + "%b", + "%A", + "%a", +) + + class StandardPromptResult(BaseModel): """Result with only prompt field for standard mode.""" @@ -87,7 +102,11 @@ class Prompts(BaseModel): slices.append("tools") else: slices.append("no_tools") - system: str = self._build_prompt(slices) + self._build_skill_block() + system: str = ( + self._build_prompt(slices) + + self._build_skill_block() + + self._build_date_block() + ) task_slice: COMPONENTS if self.use_native_tool_calling: @@ -106,7 +125,9 @@ class Prompts(BaseModel): return SystemPromptResult( system=system, user=self._build_prompt([task_slice]), - prompt=self._build_prompt(slices) + self._build_skill_block(), + prompt=self._build_prompt(slices) + + self._build_skill_block() + + self._build_date_block(), ) return StandardPromptResult( prompt=self._build_prompt( @@ -116,8 +137,31 @@ class Prompts(BaseModel): self.response_template, ) + self._build_skill_block() + + self._build_date_block() ) + def _build_date_block(self) -> str: + """Render the current date when the agent has ``inject_date`` enabled. + + Kept at the tail of the prompt so the stable prefix ahead of it stays + usable as a prompt-cache anchor. + """ + if not getattr(self.agent, "inject_date", False): + return "" + + date_format = getattr(self.agent, "date_format", "%Y-%m-%d") + try: + if not any(code in date_format for code in VALID_DATE_FORMAT_CODES): + raise ValueError(f"Invalid date format: {date_format}") + current_date = datetime.now().strftime(date_format) + except Exception as e: + logger = getattr(self.agent, "_logger", None) + if logger is not None: + logger.log("warning", f"Failed to inject date: {e!s}") + return "" + + return f"\n\nCurrent Date: {current_date}" + def _build_skill_block(self) -> str: """Render always-on instructions and the available skill catalog. diff --git a/lib/crewai/tests/agents/test_agent_inject_date.py b/lib/crewai/tests/agents/test_agent_inject_date.py index 0ca9da18f1..43d8132387 100644 --- a/lib/crewai/tests/agents/test_agent_inject_date.py +++ b/lib/crewai/tests/agents/test_agent_inject_date.py @@ -1,116 +1,165 @@ +"""Tests for the agent ``inject_date`` flag. + +These assert against the messages the LLM actually receives, so they fail if the +date stops reaching the wire for either execution entry point: crew/task +execution via ``execute_task`` and standalone execution via ``kickoff``. +""" + from datetime import datetime +from typing import Any from unittest.mock import patch from crewai.agent import Agent +from crewai.llms.base_llm import BaseLLM from crewai.task import Task -MOCK_TARGET = "crewai.agent.core.datetime" +MOCK_TARGET = "crewai.utilities.prompts.datetime" +FROZEN_NOW = datetime(2025, 1, 1) -def test_agent_inject_date(): - """Test that the inject_date flag injects the current date into the task. +class _RecordingLLM(BaseLLM): + """Deterministic LLM that captures every message list it is handed.""" - Tests that when inject_date=True, the current date is added to the task description. - """ - with patch(MOCK_TARGET) as mock_datetime: - mock_datetime.now.return_value = datetime(2025, 1, 1) + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + self.calls: list[Any] = [] - agent = Agent( - role="test_agent", - goal="test_goal", - backstory="test_backstory", - inject_date=True, + def call(self, messages: Any, **kwargs: Any) -> str: + self.calls.append(messages) + return "Thought: Done.\nFinal Answer: done" + + def supports_function_calling(self) -> bool: + return False + + def supports_stop_words(self) -> bool: + return False + + def get_context_window_size(self) -> int: + return 8_192 + + def contents_for(self, role: str) -> list[str]: + """Every message body sent under ``role`` across all calls.""" + return [ + str(message.get("content", "")) + for messages in self.calls + if not isinstance(messages, str) + for message in messages + if message.get("role") == role + ] + + @property + def everything_sent(self) -> str: + return "\n".join( + str(message.get("content", "")) + for messages in self.calls + if not isinstance(messages, str) + for message in messages ) - task = Task( - description="Test task", - expected_output="Test output", - agent=agent, - ) - original_description = task.description - - agent._inject_date_to_task(task) - - assert "Current Date: 2025-01-01" in task.description - assert task.description != original_description - - -def test_agent_without_inject_date(): - """Test that without inject_date flag, no date is injected. - - Tests that when inject_date=False (default), no date is added to the task description. - """ - agent = Agent( +def _agent(llm: BaseLLM, **kwargs: Any) -> Agent: + return Agent( role="test_agent", goal="test_goal", backstory="test_backstory", + llm=llm, + max_iter=2, + **kwargs, ) - task = Task( - description="Test task", - expected_output="Test output", + +def _task(agent: Agent) -> Task: + return Task( + description="What is the date today?", + expected_output="The date.", agent=agent, ) - original_description = task.description - agent._inject_date_to_task(task) +def test_inject_date_reaches_system_prompt_on_kickoff() -> None: + """Standalone ``kickoff`` must put the date in the system message.""" + llm = _RecordingLLM(model="date-test") + agent = _agent(llm, inject_date=True) - assert task.description == original_description - - -def test_agent_inject_date_custom_format(): - """Test that the inject_date flag with custom date_format works correctly. - - Tests that when inject_date=True with a custom date_format, the date is formatted correctly. - """ with patch(MOCK_TARGET) as mock_datetime: - mock_datetime.now.return_value = datetime(2025, 1, 1) + mock_datetime.now.return_value = FROZEN_NOW + agent.kickoff("What is the date today?") - agent = Agent( - role="test_agent", - goal="test_goal", - backstory="test_backstory", - inject_date=True, - date_format="%d/%m/%Y", - ) - - task = Task( - description="Test task", - expected_output="Test output", - agent=agent, - ) - - original_description = task.description - - agent._inject_date_to_task(task) - - assert "Current Date: 01/01/2025" in task.description - assert task.description != original_description + system_prompts = llm.contents_for("system") + assert system_prompts + assert all("Current Date: 2025-01-01" in prompt for prompt in system_prompts) -def test_agent_inject_date_invalid_format(): - """Test error handling with invalid date format. +def test_inject_date_reaches_system_prompt_on_task_execution() -> None: + """Crew/task execution must keep putting the date in front of the model.""" + llm = _RecordingLLM(model="date-test") + agent = _agent(llm, inject_date=True) - Tests that when an invalid date_format is provided, the task description remains unchanged. - """ - agent = Agent( - role="test_agent", - goal="test_goal", - backstory="test_backstory", - inject_date=True, - date_format="invalid", + with patch(MOCK_TARGET) as mock_datetime: + mock_datetime.now.return_value = FROZEN_NOW + agent.execute_task(_task(agent)) + + system_prompts = llm.contents_for("system") + assert system_prompts + assert all("Current Date: 2025-01-01" in prompt for prompt in system_prompts) + + +def test_inject_date_reaches_prompt_without_system_prompt() -> None: + """With ``use_system_prompt=False`` the whole prompt is one user message.""" + llm = _RecordingLLM(model="date-test") + agent = _agent(llm, inject_date=True, use_system_prompt=False) + + with patch(MOCK_TARGET) as mock_datetime: + mock_datetime.now.return_value = FROZEN_NOW + agent.kickoff("What is the date today?") + + assert not llm.contents_for("system") + assert "Current Date: 2025-01-01" in llm.contents_for("user")[0] + + +def test_inject_date_custom_format() -> None: + llm = _RecordingLLM(model="date-test") + agent = _agent(llm, inject_date=True, date_format="%d/%m/%Y") + + with patch(MOCK_TARGET) as mock_datetime: + mock_datetime.now.return_value = FROZEN_NOW + agent.kickoff("What is the date today?") + + assert "Current Date: 01/01/2025" in llm.contents_for("system")[0] + + +def test_without_inject_date_no_date_is_sent() -> None: + llm = _RecordingLLM(model="date-test") + agent = _agent(llm) + + agent.kickoff("What is the date today?") + + assert llm.calls + assert "Current Date:" not in llm.everything_sent + + +def test_inject_date_invalid_format_is_skipped() -> None: + """An unusable format should drop the date, not break execution.""" + llm = _RecordingLLM(model="date-test") + agent = _agent(llm, inject_date=True, date_format="invalid") + + output = agent.kickoff("What is the date today?") + + assert output.raw == "done" + assert "Current Date:" not in llm.everything_sent + + +def test_inject_date_does_not_accumulate_across_runs() -> None: + """Re-running the same task must not stack up repeated date lines.""" + llm = _RecordingLLM(model="date-test") + agent = _agent(llm, inject_date=True) + task = _task(agent) + + with patch(MOCK_TARGET) as mock_datetime: + mock_datetime.now.return_value = FROZEN_NOW + agent.execute_task(task) + agent.execute_task(task) + + assert all( + prompt.count("Current Date:") == 1 for prompt in llm.contents_for("system") ) - - task = Task( - description="Test task", - expected_output="Test output", - agent=agent, - ) - - original_description = task.description - - agent._inject_date_to_task(task) - - assert task.description == original_description