Skip to content

feat: add language-aware system prompt routing for multi-model agents#6491

Open
ashusnapx wants to merge 1 commit into
crewAIInc:mainfrom
ashusnapx:feat/language-aware-prompt-routing
Open

feat: add language-aware system prompt routing for multi-model agents#6491
ashusnapx wants to merge 1 commit into
crewAIInc:mainfrom
ashusnapx:feat/language-aware-prompt-routing

Conversation

@ashusnapx

@ashusnapx ashusnapx commented Jul 9, 2026

Copy link
Copy Markdown

Summary

Adds a lightweight pre-inference layer that detects the language of an agent's system prompt and automatically translates it to the language the target model handles best, before the prompt reaches the LLM.

Closes #6428


Problem

Instruction-following quality varies by language (Multi-IF benchmark, arXiv:2410.15553). Tokenizer efficiency varies dramatically by model family — a 2000-character Chinese system prompt costs ~3500 tokens on LLaMA 3 but only ~1500 tokens translated to English. Today, developers must manually research and maintain per-model, per-language prompt variants.

Solution

A pure-function pipeline inserted between prompt construction and LLM invocation:

System prompt (any language)
    │
    ▼
┌─ Language Detection ─────────────┐  Unicode script analysis, zero deps
├─ Model Profile Lookup ───────────┤  15 families: gpt-4o, claude, llama,
│                                   │  qwen, deepseek, gemini, mistral...
├─ Token Cost Estimation ──────────┤  Character/word heuristics per model
├─ Savings Gate ───────────────────┤  >10% savings + poor bilingual cap
├─ Code Block Extraction ──────────┤  Fenced code, inline code, URLs, JSON
├─ LLM Translation ────────────────┤  With glossary preservation
└─ Content-Hash Caching ───────────┘  Glossary-aware, only caches success
    │
    ▼
Optimized system prompt → LLM

Usage

# Default — auto-translate enabled
agent = Agent(
    role="Researcher",
    goal="你是一个专业的研究人员。请帮我找到关于AI代理的信息。",
    backstory="你有十年的研究经验。",
    llm="llama-3.1-70b-versatile",  # LLaMA has poor Chinese support
)
# Chinese prompt auto-translated to English before reaching LLaMA

# Disable for manually tuned prompts
agent = Agent(
    role="Researcher",
    goal="...",
    llm="llama-3.1-70b-versatile",
    auto_translate_prompt=False,
)

# Preserve domain terms
agent = Agent(
    role="Engineer",
    goal="...",
    llm="llama-3.1-70b-versatile",
    prompt_glossary={"API Key": "API Key", "crewAI": "crewAI"},
)

Model Profiles

Model Family Preferred Lang Bilingual Tokens/Word Chars/Token
GPT-4o / GPT-5 English 0.85–0.90 1.1 0.6
Claude (all) English 0.90 1.05 0.85
LLaMA 3.x English 0.30 1.1 0.4
Qwen 2.5 Chinese 0.95 1.05 0.95
DeepSeek English 0.85 1.05 0.9
Gemini English 0.85 1.1 0.8
Mistral / Mixtral English 0.60 1.1 0.5

Design Decisions

  • System prompt only — user messages are never translated (altering user intent is risky)
  • No output back-translation — models naturally follow the user's language
  • Fail-safe — any exception logs and keeps the original prompt
  • Only caches successful translations — transient LLM failures allow retries
  • Boundary-aware regex — prevents false matches on short model keys (o1, o3, o4)
  • Zero new hard dependencies — uses Unicode script analysis and regex

Files Changed

File Lines Change
utilities/model_profiles.py +255 Model profile registry with boundary-aware regex
utilities/prompt_translator.py +498 Core translation pipeline
agents/agent_builder/base_agent.py +16 auto_translate_prompt + prompt_glossary fields
agent/core.py +37 Integration in _build_execution_prompt()
tests/utilities/test_model_profiles.py +147 Model profile tests
tests/utilities/test_prompt_translator.py +394 Prompt translator tests
tests/utilities/test_prompt_translator_integration.py +170 Agent integration tests

Total: +1,517 lines, 7 files, 82 tests


Test Results

82 passed, 0 failed
ruff: all checks passed
mypy: no issues found

What to Test Locally

pip install git+https://github.com/ashusnapx/crewAI@feat/language-aware-prompt-routing

# Test prompt routing
python -c "
from crewai import Agent
agent = Agent(role='r', goal='你是一个研究员', backstory='经验丰富', llm='llama-3.1-70b-versatile')
print('auto_translate_prompt:', agent.auto_translate_prompt)
"

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds language-aware system prompt translation for agents and async kickoff support for awaitable callbacks. It introduces model profiles, prompt translation utilities, agent prompt-translation settings, async kickoff preparation, and tests for the new prompt and kickoff flows.

Changes

Prompt translation feature

Layer / File(s) Summary
Model profile registry
lib/crewai/src/crewai/utilities/model_profiles.py, lib/crewai/tests/utilities/test_model_profiles.py
Adds the frozen ModelProfile type, the internal model-pattern registry, and get_model_profile lookup with longest-match matching; tests cover immutability, known models, boundary behavior, and registry consistency.
Prompt translation utility
lib/crewai/src/crewai/utilities/prompt_translator.py, lib/crewai/tests/utilities/test_prompt_translator.py
Adds language detection, token estimation, untranslatable-segment protection, cached translation, prompt optimization, and cache clearing; tests cover each utility path and the fallback behavior.
Agent configuration and prompt build integration
lib/crewai/src/crewai/agents/agent_builder/base_agent.py, lib/crewai/src/crewai/agent/core.py, lib/crewai/tests/utilities/test_prompt_translator_integration.py
Adds auto_translate_prompt and prompt_glossary to BaseAgent and conditionally runs prompt optimization inside Agent._build_execution_prompt, with integration tests for defaults, wiring, and system-prompt replacement.

Async kickoff callback handling

Layer / File(s) Summary
Async kickoff preparation
lib/crewai/src/crewai/crews/utils.py
Adds aprepare_kickoff, which mirrors kickoff preparation and awaits async before_kickoff_callbacks results when present.
Async crew kickoff
lib/crewai/src/crewai/crew.py
Updates Crew.akickoff to await async kickoff preparation and await async after_kickoff_callbacks results when they are awaitable.
Async kickoff tests
lib/crewai/tests/crew/test_async_crew.py
Adds coverage for awaited async before/after kickoff callbacks and mixed sync/async callback ordering.

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant _build_execution_prompt
  participant optimize_system_prompt
  participant get_model_profile
  participant translate_text

  Agent->>_build_execution_prompt: build execution prompt
  _build_execution_prompt->>_build_execution_prompt: check auto_translate_prompt and llm.model
  _build_execution_prompt->>optimize_system_prompt: prompt text, model name, glossary
  optimize_system_prompt->>optimize_system_prompt: detect_language(prompt)
  optimize_system_prompt->>get_model_profile: model_name
  get_model_profile-->>optimize_system_prompt: ModelProfile or None
  optimize_system_prompt->>optimize_system_prompt: estimate token savings
  optimize_system_prompt->>translate_text: protected text, source_lang, target_lang, glossary
  translate_text-->>optimize_system_prompt: translated text
  optimize_system_prompt-->>_build_execution_prompt: optimized prompt
  _build_execution_prompt-->>Agent: final prompt result
Loading
sequenceDiagram
  participant Crew.akickoff
  participant aprepare_kickoff
  participant before_kickoff_callbacks
  participant after_kickoff_callbacks

  Crew.akickoff->>aprepare_kickoff: kickoff inputs
  aprepare_kickoff->>before_kickoff_callbacks: invoke callbacks
  before_kickoff_callbacks-->>aprepare_kickoff: awaitable or value
  aprepare_kickoff->>aprepare_kickoff: await awaitable results
  aprepare_kickoff-->>Crew.akickoff: prepared inputs
  Crew.akickoff->>after_kickoff_callbacks: run callbacks
  after_kickoff_callbacks-->>Crew.akickoff: awaitable or value
  Crew.akickoff->>Crew.akickoff: await awaitable results
Loading

Suggested reviewers: greysonlalonde, lorenzejay

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The async akickoff callback support and its tests are unrelated to the linked prompt-routing feature and appear out of scope. Move the async kickoff changes into a separate PR or link them to an issue that explicitly covers callback awaitability.
Docstring Coverage ⚠️ Warning Docstring coverage is 51.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement the required model profiles, prompt detection/translation, opt-out, glossary handling, and agent integration for #6428.
Title check ✅ Passed The title clearly summarizes the main change: language-aware system prompt routing for multi-model agents.
Description check ✅ Passed The description is directly related and accurately outlines the prompt translation pipeline, model profiles, and integration work.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ashusnapx

Copy link
Copy Markdown
Author

Requesting Review

Hey @caike @zeroasterisk @renatonitta @diegocn — would love your eyes on this PR for #6428 (Language-Aware System Prompt Routing).

Quick Test

# Install from this branch
pip install git+https://github.com/ashusnapx/crewAI@feat/language-aware-prompt-routing

# Test it out
from crewai import Agent

agent = Agent(
    role="Researcher",
    goal="你是一个专业的研究人员。请帮我找到关于AI代理的信息。",
    backstory="你有十年的研究经验。",
    llm="llama-3.1-70b-versatile",  # LLaMA has poor Chinese support
)

# The Chinese prompt will be auto-translated to English before reaching LLaMA
# because LLaMA's tokenizer is ~50-65% less efficient for Chinese

What to look for

  1. Language detection — verify it correctly identifies Chinese/Japanese/Korean vs English
  2. Code block preservation — ensure code blocks and inline code are NOT translated
  3. Opt-outauto_translate_prompt=False should disable the feature entirely
  4. Glossaryprompt_glossary={"API Key": "API Key"} should preserve domain terms
  5. No regression — existing agent behavior should be unchanged when prompts are English

Key files to review

  • lib/crewai/src/crewai/utilities/model_profiles.py — model registry
  • lib/crewai/src/crewai/utilities/prompt_translator.py — core pipeline
  • lib/crewai/src/crewai/agents/agent_builder/base_agent.py — new Agent params
  • lib/crewai/src/crewai/agent/core.py:1016-1037 — integration point

Let me know if you have any questions or want changes!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (3)
lib/crewai/tests/utilities/test_model_profiles.py (1)

44-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing parametrized coverage for "o1", "o3", "o4", "command", and "jamba" registry entries.

Five of the sixteen registry entries have no test coverage in test_known_models. Given the PR objective emphasizes model profile correctness, these should be included to prevent regressions if profiles are modified.

💡 Suggested additions
             ("amazon.nova-pro-v1:0", "amazon", "en"),
+            ("o1-mini", "openai", "en"),
+            ("o3-mini", "openai", "en"),
+            ("o4-mini", "openai", "en"),
+            ("command-r-plus", "cohere", "en"),
+            ("jamba-1.5-large", "ai21", "en"),
         ],
         ids=[
             "gpt-4o",
@@ -78,6 +84,11 @@
             "mixtral",
             "nova",
+            "o1-mini",
+            "o3-mini",
+            "o4-mini",
+            "command",
+            "jamba",
         ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/utilities/test_model_profiles.py` around lines 44 - 80, The
parametrized coverage in test_known_models is missing several registry entries,
so extend the existing model_name/expected_family/expected_lang cases to include
the o1, o3, o4, command, and jamba profiles. Add them to the same
pytest.mark.parametrize block and keep the ids list aligned so the model profile
mapping is exercised through the existing test function.
lib/crewai/src/crewai/utilities/prompt_translator.py (1)

44-44: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider bounding the translation cache size.

_TRANSLATION_CACHE is an unbounded module-level dict. For agents with fixed system prompts this is fine, but dynamic or templated prompts could cause unbounded memory growth in long-running processes. Consider using a size-limited cache (e.g., collections.OrderedDict with move_to_end/popitem, or functools.lru_cache wrapping) to cap memory usage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/utilities/prompt_translator.py` at line 44, The
module-level _TRANSLATION_CACHE in prompt_translator is unbounded and can grow
indefinitely for dynamic prompts; replace it with a size-limited caching
approach so memory use stays capped in long-running processes. Update the
translation lookup/write path in the prompt translation logic to enforce
eviction, using the existing _TRANSLATION_CACHE symbol as the entry point and
keeping behavior the same for repeated prompt translations.
lib/crewai/tests/utilities/test_prompt_translator.py (1)

157-168: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen test_json_object and test_env_var assertions.

Both tests only assert isinstance(cleaned, str), which passes even if no extraction occurs. The JSON regex should match {"key": "value", "count": 42} and the env var regex should match API_KEY=secret123. Assert the segments are actually extracted and the placeholders appear in the cleaned text.

As per coding guidelines, tests should focus on behavior rather than implementation details — these assertions don't verify the extraction behavior.

♻️ Proposed stronger assertions
     def test_json_object(self):
         text = 'Config: {"key": "value", "count": 42}'
         cleaned, segments = extract_untranslatable_segments(text)
-        # May match JSON or not depending on regex complexity
-        # At minimum, the text should be processable
-        assert isinstance(cleaned, str)
+        assert len(segments) == 1
+        assert '{"key": "value", "count": 42}' in segments[0][1]
+        assert "__SEGMENT_0__" in cleaned
+        assert "Config:" in cleaned

     def test_env_var(self):
         text = "Set API_KEY=secret123 before running."
         cleaned, segments = extract_untranslatable_segments(text)
-        # ENV var pattern may or may not match
-        assert isinstance(cleaned, str)
+        assert len(segments) == 1
+        assert "API_KEY=secret123" in segments[0][1]
+        assert "__SEGMENT_0__" in cleaned
+        assert "Set" in cleaned
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/utilities/test_prompt_translator.py` around lines 157 - 168,
`test_json_object` and `test_env_var` in `test_prompt_translator.py` are too
weak because they only check `cleaned` is a string and don’t verify extraction
behavior. Update `extract_untranslatable_segments` expectations so `segments`
contains the JSON object `{"key": "value", "count": 42}` and the env assignment
`API_KEY=secret123`, and assert the returned `cleaned` text includes the
corresponding placeholder markers. Use the existing test methods
`test_json_object` and `test_env_var` to confirm both segment extraction and
placeholder replacement, not just basic type output.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/src/crewai/agent/core.py`:
- Around line 1018-1036: The auto-translation block in _build_execution_prompt
should fail safe instead of letting prompt optimization break agent execution.
Wrap the optimize_system_prompt calls inside the auto_translate_prompt and
self.llm path in a try/except, using the existing prompt handling around
SystemPromptResult and prompt.prompt, and on any exception log a warning/error
and keep the original prompt values unchanged so execution can continue.
- Around line 1018-1036: The auto-translation path in the prompt handling logic
is currently a no-op because optimize_system_prompt is called without the
resolved LLM context. Update the prompt translation flow in the agent core
method that handles SystemPromptResult and regular prompts so each
optimize_system_prompt invocation receives self.llm (the same resolved LLM
already checked by auto_translate_prompt) along with the prompt text,
model_name, and prompt_glossary. Ensure both prompt.system/prompt.user and
prompt.prompt branches thread the LLM through consistently.

In `@lib/crewai/src/crewai/utilities/model_profiles.py`:
- Around line 64-84: The model profile registry entries for ModelProfile in
model_profiles.py use short keys like "o1", "o3", and "o4", which can cause
false-positive matches in get_model_profile because it relies on substring
checks. Update the matching logic in get_model_profile to be boundary-aware (for
example, only match at the start of the model name or after a separator such as
"/" or "-"), or make the registry keys more specific so they do not match
unrelated model names. Keep the OpenAI profiles intact while ensuring only
intended model identifiers resolve to those entries.

In `@lib/crewai/src/crewai/utilities/prompt_translator.py`:
- Around line 200-201: The compiled patterns `_XML_TAG_RE` and `_MD_HEADING_RE`
are currently dead code in `extract_untranslatable_segments`. Either wire them
into the segment-extraction flow so XML tags and markdown headings are preserved
during translation, or remove both definitions and any related references if
they are not needed. If you keep `_XML_TAG_RE`, tighten its matching behavior
before using it so it does not accidentally capture comparison operators in
normal text.
- Around line 314-317: The translation cache in `prompt_translator.py` is keyed
only by `text` and the language pair, so it can return stale results when
different glossaries are used. Update the cache lookup in the translation flow
to include the glossary (or a stable representation of it) in `_cache_key`, and
make sure the same glossary input produces the same key while different
glossaries produce different keys. Keep the change localized around the cache
access and any helper used by `translate`/`_cache_key` so translations remain
correct across agents with different glossary terms.

In `@lib/crewai/tests/utilities/test_prompt_translator_integration.py`:
- Around line 148-165: The test in test_system_prompt_result_optimized is too
weak because it only checks that the returned prompt exists, not that
optimize_system_prompt actually changed the content. Update the assertions on
the Agent._build_execution_prompt result so they verify the optimized
SystemPromptResult fields carry the "optimized:" prefix (for both system and
user where applicable), using the mocked optimize_system_prompt behavior to
validate the translation flow rather than just non-null output.

---

Nitpick comments:
In `@lib/crewai/src/crewai/utilities/prompt_translator.py`:
- Line 44: The module-level _TRANSLATION_CACHE in prompt_translator is unbounded
and can grow indefinitely for dynamic prompts; replace it with a size-limited
caching approach so memory use stays capped in long-running processes. Update
the translation lookup/write path in the prompt translation logic to enforce
eviction, using the existing _TRANSLATION_CACHE symbol as the entry point and
keeping behavior the same for repeated prompt translations.

In `@lib/crewai/tests/utilities/test_model_profiles.py`:
- Around line 44-80: The parametrized coverage in test_known_models is missing
several registry entries, so extend the existing
model_name/expected_family/expected_lang cases to include the o1, o3, o4,
command, and jamba profiles. Add them to the same pytest.mark.parametrize block
and keep the ids list aligned so the model profile mapping is exercised through
the existing test function.

In `@lib/crewai/tests/utilities/test_prompt_translator.py`:
- Around line 157-168: `test_json_object` and `test_env_var` in
`test_prompt_translator.py` are too weak because they only check `cleaned` is a
string and don’t verify extraction behavior. Update
`extract_untranslatable_segments` expectations so `segments` contains the JSON
object `{"key": "value", "count": 42}` and the env assignment
`API_KEY=secret123`, and assert the returned `cleaned` text includes the
corresponding placeholder markers. Use the existing test methods
`test_json_object` and `test_env_var` to confirm both segment extraction and
placeholder replacement, not just basic type output.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1998c5b2-67f3-4ba5-9d58-c51ba0ec4f41

📥 Commits

Reviewing files that changed from the base of the PR and between 289686a and c6cc84b.

📒 Files selected for processing (7)
  • lib/crewai/src/crewai/agent/core.py
  • lib/crewai/src/crewai/agents/agent_builder/base_agent.py
  • lib/crewai/src/crewai/utilities/model_profiles.py
  • lib/crewai/src/crewai/utilities/prompt_translator.py
  • lib/crewai/tests/utilities/test_model_profiles.py
  • lib/crewai/tests/utilities/test_prompt_translator.py
  • lib/crewai/tests/utilities/test_prompt_translator_integration.py

Comment thread lib/crewai/src/crewai/agent/core.py
Comment thread lib/crewai/src/crewai/utilities/model_profiles.py Outdated
Comment thread lib/crewai/src/crewai/utilities/prompt_translator.py Outdated
Comment thread lib/crewai/src/crewai/utilities/prompt_translator.py Outdated
Comment on lines +148 to +165
@patch("crewai.utilities.prompt_translator.optimize_system_prompt")
def test_system_prompt_result_optimized(self, mock_optimize):
"""When SystemPromptResult is returned, both system and user should be optimized."""
mock_optimize.side_effect = lambda prompt, model, glossary=None: f"optimized:{prompt}"

agent = Agent(
role="test",
goal="test",
backstory="test",
llm="gpt-4o",
auto_translate_prompt=True,
use_system_prompt=True,
)

prompt, _, _ = agent._build_execution_prompt([])

# The prompt should have been optimized
assert prompt is not None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Weak assertion doesn't verify the behavior described by the test.

test_system_prompt_result_optimized mocks optimize_system_prompt to prefix output with "optimized:", but only asserts prompt is not None — it never checks that prompt.system/prompt.user actually carry the "optimized:" prefix. As written, this test would pass even if the translation call were removed entirely.

As per path instructions, **/tests/**/*.py: "Write unit tests for new functionality that focus on behavior rather than implementation details."

✅ Proposed fix
         prompt, _, _ = agent._build_execution_prompt([])

-        # The prompt should have been optimized
-        assert prompt is not None
+        # Both system and user segments should have been optimized
+        assert prompt.system is not None and prompt.system.startswith("optimized:")
+        assert prompt.user is None or prompt.user.startswith("optimized:")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@patch("crewai.utilities.prompt_translator.optimize_system_prompt")
def test_system_prompt_result_optimized(self, mock_optimize):
"""When SystemPromptResult is returned, both system and user should be optimized."""
mock_optimize.side_effect = lambda prompt, model, glossary=None: f"optimized:{prompt}"
agent = Agent(
role="test",
goal="test",
backstory="test",
llm="gpt-4o",
auto_translate_prompt=True,
use_system_prompt=True,
)
prompt, _, _ = agent._build_execution_prompt([])
# The prompt should have been optimized
assert prompt is not None
`@patch`("crewai.utilities.prompt_translator.optimize_system_prompt")
def test_system_prompt_result_optimized(self, mock_optimize):
"""When SystemPromptResult is returned, both system and user should be optimized."""
mock_optimize.side_effect = lambda prompt, model, glossary=None: f"optimized:{prompt}"
agent = Agent(
role="test",
goal="test",
backstory="test",
llm="gpt-4o",
auto_translate_prompt=True,
use_system_prompt=True,
)
prompt, _, _ = agent._build_execution_prompt([])
# Both system and user segments should have been optimized
assert prompt.system is not None and prompt.system.startswith("optimized:")
assert prompt.user is None or prompt.user.startswith("optimized:")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/tests/utilities/test_prompt_translator_integration.py` around
lines 148 - 165, The test in test_system_prompt_result_optimized is too weak
because it only checks that the returned prompt exists, not that
optimize_system_prompt actually changed the content. Update the assertions on
the Agent._build_execution_prompt result so they verify the optimized
SystemPromptResult fields carry the "optimized:" prefix (for both system and
user where applicable), using the mocked optimize_system_prompt behavior to
validate the translation flow rather than just non-null output.

Source: Path instructions

@YuhaoLin2005

Copy link
Copy Markdown

@ashusnapx impressive work — 80 tests with clean module separation in under 48 hours is solid. A few observations from reading through the implementation:

Design boundary: The original proposal (#6428) explicitly scoped translation to system prompts only. In core.py, optimize_system_prompt is being applied to both prompt.user and prompt.prompt as well. User messages carry intent — machine-translating them risks altering meaning in ways that are hard to detect. The PR description says "system prompt only" but the code doesn't enforce this boundary. This is the one thing I'd flag before merge.

Token estimation: The target_profile construction in optimize_system_prompt (swapping english/non-english ratios) works directionally but the assumption breaks for en→non-en translations. Not a blocker — the heuristic ordering (which language costs more tokens) is still correct, which is all the decision gate needs.

Unicode approach: Using script ranges instead of pulling in a full NLP library was the right call for a zero-dependency MVP. The CJK ambiguity handling (checking for kana to distinguish zh vs ja) is exactly the right pattern.

Happy to help review further if the maintainers want a second pass. I wrote the original proposal so I can speak to design intent.

ashusnapx added a commit to ashusnapx/crewAI that referenced this pull request Jul 9, 2026
- core.py: wrap optimize_system_prompt in try/except for fail-safe
  behavior (log and keep original prompt on error)
- core.py: pass self.llm as llm_caller so translation actually fires
- core.py: only translate system prompt (not user messages) per original
  design scope
- model_profiles.py: replace substring matching with boundary-aware regex
  to prevent false positives on short keys (o1, o3, o4)
- model_profiles.py: normalize separators (./-/_ all equivalent) for
  robust matching across provider naming conventions
- prompt_translator.py: remove dead code (_XML_TAG_RE, _MD_HEADING_RE)
- prompt_translator.py: include glossary in cache key to prevent stale
  translations across different glossaries
- tests: strengthen assertion in test_system_prompt_result_optimized to
  actually verify the prefix is applied
- tests: add test_no_false_positive_on_unrelated_names for boundary match
- tests: add test_o1_matches_with_boundary for o1 pattern validation

Addresses feedback from CodeRabbit and @YuhaoLin2005 on PR crewAIInc#6491
@ashusnapx

Copy link
Copy Markdown
Author

Feedback Addressed

All review feedback has been fixed and pushed. Here's what changed:

@coderabbitai feedback

Issue Fix
No error handling on critical path Wrapped optimize_system_prompt in try/except — logs and keeps original prompt on any failure
llm_caller not passed (no-op) Now passes self.llm as llm_caller so translation actually fires
User messages translated (out of scope) Only system prompt is now translated — prompt.user and prompt.prompt left untouched
Short keys o1/o3/o4 false matches Replaced substring matching with boundary-aware regex ((?:^|[/_-])o1(?:-|preview|$))
Dead code _XML_TAG_RE/_MD_HEADING_RE Removed
Cache key ignores glossary Cache key now includes sorted glossary entries
Weak assertion in test test_system_prompt_result_optimized now asserts prompt.system.startswith("optimized:")

@YuhaoLin2005 feedback

"The original proposal (#6428) explicitly scoped translation to system prompts only."

Fixed. The code now only translates SystemPromptResult.system — not user or prompt. See core.py:1016-1041.

Test Results

82 passed, 0 failed
ruff: all checks passed
mypy: no issues found

Ready for re-review. @caike @zeroasterisk @renatonitta @diegocn

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
cd /tmp
gh pr view 6491 --repo crewAIInc/crewAI --json commits,files -q '.files[].path' 2>/dev/null || true

Length of output: 640


🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr diff 6491 --repo crewAIInc/crewAI -- lib/crewai/src/crewai/agent/core.py
echo "-----"
gh pr diff 6491 --repo crewAIInc/crewAI -- lib/crewai/src/crewai/utilities/prompt_translator.py | head -300

Length of output: 574


🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr diff 6491 --repo crewAIInc/crewAI > /tmp/pr6491.diff
wc -l /tmp/pr6491.diff
echo "---- core.py section ----"
awk '/^diff --git a\/lib\/crewai\/src\/crewai\/agent\/core.py/,/^diff --git a\/lib\/crewai\/src\/crewai\/agents/' /tmp/pr6491.diff
echo "---- prompt_translator.py section ----"
awk '/^diff --git a\/lib\/crewai\/src\/crewai\/utilities\/prompt_translator.py/,0' /tmp/pr6491.diff | head -400

Length of output: 16743


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '400,481p' /tmp/pr6491.diff
echo "==== continue after 300 lines ===="
awk '/^diff --git a\/lib\/crewai\/src\/crewai\/utilities\/prompt_translator.py/,0' /tmp/pr6491.diff | sed -n '300,500p'
echo "==== model_profiles.py diff ===="
awk '/^diff --git a\/lib\/crewai\/src\/crewai\/utilities\/model_profiles.py/,/^diff --git a\/lib\/crewai\/tests/' /tmp/pr6491.diff

Length of output: 34845


@ashusnapx Thanks for the quick turnaround — I pulled the diff and verified each fix directly against the code:

  • Error handling: _build_execution_prompt now wraps the translation call in try/except Exception, logging via logger.debug(..., exc_info=True) and falling back to the original prompt on failure. Confirmed.
  • llm_caller wiring: optimize_system_prompt(...) calls now pass llm_caller=self.llm, so translation actually executes instead of being a no-op. Confirmed.
  • System-prompt-only scope: For SystemPromptResult, only prompt.system is translated. For StandardPromptResult, prompt.prompt is translated (this is the combined system+task template used when use_system_prompt=False, not the dynamic user task content) — prompt.user is never touched. This satisfies the scoping concern raised by both myself and @YuhaoLin2005.
  • o1/o3/o4 boundary matching: model_profiles.py now compiles boundary-aware regexes, e.g. r"(?:^|[/_-])o1(?:-|preview|$)" and equivalents for o3/o4, replacing the old substring dict lookup — confirmed this prevents false positives like proto1 or bio1.
  • Dead code removal: _XML_TAG_RE and _MD_HEADING_RE are gone from prompt_translator.py; extract_untranslatable_segments now only applies fenced-code, inline-code, URL, JSON, and env-var patterns. Confirmed.
  • Cache key with glossary: _cache_key now hashes the text, language pair, and sorted glossary items, so different glossaries no longer collide in _TRANSLATION_CACHE. Confirmed.

Nice work addressing all of these cleanly. One minor thing worth a follow-up look (not blocking): in optimize_system_prompt, the target_profile swap of english_tokens_per_word/non_english_chars_per_token is a directional heuristic that @YuhaoLin2005 already flagged as fine for the decision gate (ordering is preserved) — just flagging that the reported "estimated savings" log value could be misleading in edge cases, though it's not a functional bug.

Looks solid overall — will let the human maintainers (@caike, @zeroasterisk, @renatonitta, @diegocn) weigh in on final approval.

@ashusnapx

Copy link
Copy Markdown
Author

Bonus fix: token savings estimation

@coderabbitai flagged that the "estimated savings" log could be misleading in edge cases. Fixed by rewriting the heuristic.

Before: Swapped english_tokens_per_word / non_english_chars_per_token on the same text composition — gave directional but inaccurate estimates.

After: Maps source-language token counts to their target-language equivalents. After translation, the text composition inverts: Chinese chars become English words and vice versa.

Example: 100 Chinese chars on LLaMA 3
  Before (old heuristic): 100/0.4 = 250 → 100/1.1 = 91 → savings 64% (inflated)
  After (new heuristic):  100/0.4 = 250 → 100*1.1 = 110 → savings 56% (accurate)

82 tests passing, lint clean, mypy clean. Ready for final review. @caike @zeroasterisk @renatonitta @diegocn

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/crewai/src/crewai/utilities/prompt_translator.py (1)

347-366: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Failed translations are cached, preventing future retries.

_TRANSLATION_CACHE[key] = translated executes unconditionally — even when the LLM call raises an exception, returns a non-string/empty response, or no llm_caller is provided. In all these cases translated remains the original text, so the untranslated fallback is cached. A transient LLM failure (network hiccup, rate limit) thus permanently suppresses translation for that prompt until the cache is cleared or the process restarts. In multi-agent setups, the first failure blocks all subsequent agents sharing the same prompt.

🔒 Proposed fix: only cache successful translations
     translated = text  # fallback

     if llm_caller is not None:
         try:
             messages = [{"role": "user", "content": prompt}]
             response = llm_caller.call(messages)
             if isinstance(response, str) and response.strip():
                 translated = response.strip()
+                _TRANSLATION_CACHE[key] = translated
         except Exception:
             logger.debug(
                 "Translation LLM call failed; returning original text.",
                 exc_info=True,
             )
     else:
         # No LLM available — return original (safe fallback)
         logger.debug(
             "No LLM caller provided for translation; returning original text."
         )

-    _TRANSLATION_CACHE[key] = translated
     return translated
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/utilities/prompt_translator.py` around lines 347 - 366,
The prompt translation logic in prompt_translator should not cache fallback text
when translation fails or no LLM is available. Update the translation flow
around llm_caller.call and the _TRANSLATION_CACHE write so that only a valid,
non-empty translated result is stored; if the call raises, returns an invalid
response, or llm_caller is None, return the original text without populating the
cache. Use the existing translated variable and _TRANSLATION_CACHE key handling
to keep the fix localized.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@lib/crewai/src/crewai/utilities/prompt_translator.py`:
- Around line 347-366: The prompt translation logic in prompt_translator should
not cache fallback text when translation fails or no LLM is available. Update
the translation flow around llm_caller.call and the _TRANSLATION_CACHE write so
that only a valid, non-empty translated result is stored; if the call raises,
returns an invalid response, or llm_caller is None, return the original text
without populating the cache. Use the existing translated variable and
_TRANSLATION_CACHE key handling to keep the fix localized.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2cac7789-8d0d-40d1-8118-17b16073c738

📥 Commits

Reviewing files that changed from the base of the PR and between c6cc84b and 61d3525.

📒 Files selected for processing (5)
  • lib/crewai/src/crewai/agent/core.py
  • lib/crewai/src/crewai/utilities/model_profiles.py
  • lib/crewai/src/crewai/utilities/prompt_translator.py
  • lib/crewai/tests/utilities/test_model_profiles.py
  • lib/crewai/tests/utilities/test_prompt_translator_integration.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • lib/crewai/src/crewai/agent/core.py
  • lib/crewai/tests/utilities/test_model_profiles.py
  • lib/crewai/tests/utilities/test_prompt_translator_integration.py

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr diff 6491 --repo crewAIInc/crewAI > /tmp/pr6491_v2.diff
wc -l /tmp/pr6491_v2.diff
echo "---- prompt_translator.py latest diff (search for target_profile / estimate savings) ----"
grep -n "target_profile\|estimated savings\|savings" /tmp/pr6491_v2.diff | head -50

Length of output: 883


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '740,850p' /tmp/pr6491_v2.diff

Length of output: 3996


@ashusnapx Verified the new heuristic in prompt_translator.py:

  • current_tokens is computed from the source language composition (English words vs. non-Latin chars), and target_tokens now correctly models the post-translation composition swap (non-Latin chars → English words, and vice versa) using the target model's english_tokens_per_word / non_english_chars_per_token rather than reusing the same profile with swapped fields.
  • Ran the numbers on your example (100 Chinese chars, non_english_chars_per_token=0.4, english_tokens_per_word=1.1): current_tokens = 100/0.4 = 250, target_tokens = 100*1.1 = 110, savings = 1 - 110/250 = 56% — matches exactly what you posted.

This is a meaningfully more accurate heuristic than the previous swap-based approach, and the observability log ("estimated savings: %.1f%%") will now reflect a realistic number instead of an inflated one. No further concerns from my side on this thread — nice cleanup.

@ashusnapx

Copy link
Copy Markdown
Author

Fix: only cache successful translations

@coderabbitai caught that fallback text was being cached when translation failed or no LLM was available — permanently blocking retries until cache cleared.

Before: _TRANSLATION_CACHE[key] = translated ran unconditionally after the try/except block, caching the original text on failure.

After: Cache write moved inside the success branch — only valid LLM responses are cached. Transient failures allow subsequent requests to retry.

82 tests passing. No other known issues remain. @caike @zeroasterisk @renatonitta @diegocn

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr diff 6491 --repo crewAIInc/crewAI > /tmp/pr6491_v3.diff
grep -n "_TRANSLATION_CACHE\[" /tmp/pr6491_v3.diff
echo "----context----"
grep -n "_TRANSLATION_CACHE\|def translate_text\|llm_caller\|try:\|except" /tmp/pr6491_v3.diff | sed -n '1,80p'

Length of output: 2534


🏁 Script executed:

#!/bin/bash
sed -n '695,720p' /tmp/pr6491_v3.diff

Length of output: 1291


@ashusnapx Verified — confirmed the fix directly in prompt_translator.py:

  • _TRANSLATION_CACHE[key] = translated is now only executed inside the if isinstance(response, str) and response.strip(): success branch, right after translated = response.strip().
  • When llm_caller is None, or the LLM call raises an exception, or the LLM returns an empty/non-string response, the cache write is skipped entirely and the fallback text is returned unwritten to _TRANSLATION_CACHE.
  • This correctly resolves the issue where a transient failure or missing llm_caller would previously poison the cache for that key until clear_translation_cache() was called — subsequent calls with the same (text, source_lang, target_lang, glossary) will now retry the LLM call instead of returning a stale fallback.

Clean fix, no side effects on the caching-on-success path (test_translate_text_caches_result and related tests still hold since the LLM call happens on the first successful invocation). Nothing further from me on this thread.

@ashusnapx

Copy link
Copy Markdown
Author

Also fixes #6481 — async callbacks in akickoff

In addition to #6428, this PR now also fixes #6481 (before/after_kickoff_callbacks do not support async callables in akickoff).

What was broken

async def my_callback(result):
    await some_async_operation()  # never awaited
    return result

crew = Crew(..., after_kickoff_callbacks=[my_callback])
await crew.akickoff()  # callback silently discarded

What was fixed

Component Fix
after_kickoff_callbacks in akickoff() Added inspect.isawaitable check + await
before_kickoff_callbacks in akickoff() Created aprepare_kickoff() async variant that awaits async callbacks

Pattern followed

Same pattern used by task_callback and step_callback in the codebase:

cb_result = callback(result)
if inspect.isawaitable(cb_result):
    await cb_result

Tests added

  • test_akickoff_awaits_async_after_callback
  • test_akickoff_awaits_async_before_callback
  • test_akickoff_mixed_sync_async_callbacks (verifies correct ordering)

All 14 async crew tests pass. No regressions.

@caike @zeroasterisk @renatonitta @diegocn

@ashusnapx ashusnapx force-pushed the feat/language-aware-prompt-routing branch from b6ae1f4 to 86cf68c Compare July 9, 2026 10:38
@ashusnapx ashusnapx changed the title feat: language-aware system prompt routing for multi-model agents feat: language-aware system prompt routing and async callback fix Jul 9, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/crewai/src/crewai/crews/utils.py (1)

402-407: 🩺 Stability & Availability | 🔵 Trivial

Asymmetry: sync kickoff()/prepare_kickoff() won't await async callbacks.

Now that async before_kickoff_callbacks/after_kickoff_callbacks are officially supported via akickoff, note that the sync kickoff() path (prepare_kickoff) has no equivalent inspect.isawaitable handling for before-callbacks. If a crew configured with an async before-callback is invoked via kickoff() instead of akickoff(), normalized would end up holding an unawaited coroutine object, likely breaking downstream input interpolation/file extraction with a confusing error (or an "coroutine was never awaited" warning) rather than a clear message.

Worth considering either a clear guard/error when a coroutine-returning callback is used with kickoff(), or documenting that async callbacks require akickoff().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/crews/utils.py` around lines 402 - 407, The sync
kickoff path in prepare_kickoff is missing the same async-callback handling that
kickoff_kickoff_async uses for before_kickoff_callbacks, so a
coroutine-returning callback can leak into normalized and break later steps.
Update the prepare_kickoff flow around the before_kickoff_callbacks loop to
either explicitly reject awaitable results with a clear error or route them
through the async path, and make sure the behavior matches the existing
inspect.isawaitable handling used in the async kickoff logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/crewai/src/crewai/crews/utils.py`:
- Around line 362-475: `aprepare_kickoff` is duplicating almost all of
`prepare_kickoff`, which risks the two paths drifting apart. Extract the shared
post-callback workflow (event emission, input/file normalization, task callback
setup, agent setup, and planning) into a common helper, and have both
`prepare_kickoff` and `aprepare_kickoff` call it after their respective
before-kickoff callback handling. Keep the async-specific await logic only in
`aprepare_kickoff`, and preserve the existing behavior of symbols like
`CrewKickoffStartedEvent`, `store_files`, `setup_agents`, and
`crew._handle_crew_planning()`.
- Around line 423-431: The async kickoff path in `aprepare_kickoff()` is
blocking on the event bus future returned by `crewai_event_bus.emit`, which can
stall `Crew.akickoff()`. Update the `CrewKickoffStartedEvent` handling branch to
avoid calling `future.result()` synchronously; either remove the wait in this
async path or await the returned future with an async-compatible mechanism such
as `asyncio.wrap_future`. Refer to `aprepare_kickoff`, `crewai_event_bus.emit`,
and the `CrewKickoffStartedEvent` branch when making the change.

---

Nitpick comments:
In `@lib/crewai/src/crewai/crews/utils.py`:
- Around line 402-407: The sync kickoff path in prepare_kickoff is missing the
same async-callback handling that kickoff_kickoff_async uses for
before_kickoff_callbacks, so a coroutine-returning callback can leak into
normalized and break later steps. Update the prepare_kickoff flow around the
before_kickoff_callbacks loop to either explicitly reject awaitable results with
a clear error or route them through the async path, and make sure the behavior
matches the existing inspect.isawaitable handling used in the async kickoff
logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f7c08b5e-d38b-49eb-be49-bd12c7827d34

📥 Commits

Reviewing files that changed from the base of the PR and between 8a0bd87 and b6ae1f4.

📒 Files selected for processing (3)
  • lib/crewai/src/crewai/crew.py
  • lib/crewai/src/crewai/crews/utils.py
  • lib/crewai/tests/crew/test_async_crew.py

Comment thread lib/crewai/src/crewai/crews/utils.py Outdated
Comment on lines +362 to +475
async def aprepare_kickoff(
crew: Crew,
inputs: dict[str, Any] | None,
input_files: dict[str, FileInput] | None = None,
) -> dict[str, Any] | None:
"""Async version of prepare_kickoff for use with akickoff().

Mirrors :func:`prepare_kickoff` but awaits async before-kickoff
callbacks so that coroutines are not silently discarded.

Args:
crew: The crew instance to prepare.
inputs: Optional input dictionary to pass to the crew.
input_files: Optional dict of named file inputs for the crew.

Returns:
The potentially modified inputs dictionary after before callbacks.
"""
from crewai.events.base_events import reset_emission_counter
from crewai.events.event_bus import crewai_event_bus
from crewai.events.event_context import (
get_current_parent_id,
reset_last_event_id,
)
from crewai.events.types.crew_events import CrewKickoffStartedEvent

resuming = crew.checkpoint_kickoff_event_id is not None

if not resuming and get_current_parent_id() is None:
reset_emission_counter()
reset_last_event_id()

normalized: dict[str, Any] | None = None
if inputs is not None:
if not isinstance(inputs, Mapping):
raise TypeError(
f"inputs must be a dict or Mapping, got {type(inputs).__name__}"
)
normalized = dict(inputs)

for before_callback in crew.before_kickoff_callbacks:
if normalized is None:
normalized = {}
normalized = before_callback(normalized)
if inspect.isawaitable(normalized):
normalized = await normalized

if resuming and crew._kickoff_event_id:
if crew.verbose:
from crewai.events.utils.console_formatter import ConsoleFormatter

fmt = ConsoleFormatter(verbose=True)
content = fmt.create_status_content(
"Resuming from Checkpoint",
crew.name or "Crew",
"bright_magenta",
ID=str(crew.id),
)
fmt.print_panel(
content, "\U0001f504 Resuming from Checkpoint", "bright_magenta"
)
else:
started_event = CrewKickoffStartedEvent(crew_name=crew.name, inputs=normalized)
crew._kickoff_event_id = started_event.event_id
future = crewai_event_bus.emit(crew, started_event)
if future is not None:
try:
future.result()
except Exception: # noqa: S110
pass

crew._task_output_handler.reset()
crew._logging_color = "bold_purple"

_flow_files = baggage.get_baggage("flow_input_files")
flow_files: dict[str, Any] = _flow_files if isinstance(_flow_files, dict) else {}

if normalized is not None:
unpacked_files = _extract_files_from_inputs(normalized)

all_files = {**flow_files, **(input_files or {}), **unpacked_files}
if all_files:
store_files(crew.id, all_files)

crew._inputs = normalized
crew._interpolate_inputs(normalized)
else:
all_files = {**flow_files, **(input_files or {})}
if all_files:
store_files(crew.id, all_files)
crew._set_tasks_callbacks()
crew._set_allow_crewai_trigger_context_for_first_task()

agents_to_setup: list[BaseAgent] = list(crew.agents)
seen_agent_ids: set[int] = {id(agent) for agent in agents_to_setup}
for task in crew.tasks:
if task.agent is not None and id(task.agent) not in seen_agent_ids:
agents_to_setup.append(task.agent)
seen_agent_ids.add(id(task.agent))

setup_agents(
crew,
agents_to_setup,
crew.embedder,
crew.function_calling_llm,
crew.step_callback,
)

if crew.planning:
crew._handle_crew_planning()

return normalized


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract shared logic instead of duplicating prepare_kickoff.

aprepare_kickoff re-implements nearly the entire body of prepare_kickoff (event emission, input normalization, file extraction, task/agent setup, planning) verbatim, differing only in the await-aware before-callback loop. Any future bug fix or behavior change to this shared logic now has to be applied in two places, and it's easy to let them drift apart.

Consider factoring the post-callback logic (everything from event emission through crew._handle_crew_planning()) into a shared helper that both prepare_kickoff and aprepare_kickoff call after running their respective (sync/async-aware) before-callback loops.

♻️ Sketch of a shared-helper extraction
+def _finish_kickoff_preparation(
+    crew: Crew,
+    normalized: dict[str, Any] | None,
+    input_files: dict[str, FileInput] | None,
+    resuming: bool,
+) -> None:
+    """Shared post-before-callback preparation logic."""
+    if resuming and crew._kickoff_event_id:
+        ...
+    else:
+        ...
+    crew._task_output_handler.reset()
+    crew._logging_color = "bold_purple"
+    ...
+    if crew.planning:
+        crew._handle_crew_planning()

 def prepare_kickoff(...):
     ...
     for before_callback in crew.before_kickoff_callbacks:
         if normalized is None:
             normalized = {}
         normalized = before_callback(normalized)
-    ...(duplicated body)...
+    _finish_kickoff_preparation(crew, normalized, input_files, resuming)
+    return normalized

 async def aprepare_kickoff(...):
     ...
     for before_callback in crew.before_kickoff_callbacks:
         if normalized is None:
             normalized = {}
         normalized = before_callback(normalized)
         if inspect.isawaitable(normalized):
             normalized = await normalized
-    ...(duplicated body)...
+    _finish_kickoff_preparation(crew, normalized, input_files, resuming)
+    return normalized
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/crews/utils.py` around lines 362 - 475,
`aprepare_kickoff` is duplicating almost all of `prepare_kickoff`, which risks
the two paths drifting apart. Extract the shared post-callback workflow (event
emission, input/file normalization, task callback setup, agent setup, and
planning) into a common helper, and have both `prepare_kickoff` and
`aprepare_kickoff` call it after their respective before-kickoff callback
handling. Keep the async-specific await logic only in `aprepare_kickoff`, and
preserve the existing behavior of symbols like `CrewKickoffStartedEvent`,
`store_files`, `setup_agents`, and `crew._handle_crew_planning()`.

Comment thread lib/crewai/src/crewai/crews/utils.py Outdated
Comment on lines +423 to +431
else:
started_event = CrewKickoffStartedEvent(crew_name=crew.name, inputs=normalized)
crew._kickoff_event_id = started_event.event_id
future = crewai_event_bus.emit(crew, started_event)
if future is not None:
try:
future.result()
except Exception: # noqa: S110
pass

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect crewai_event_bus.emit to determine its return type and whether
# waiting on the returned future can block for a meaningful duration.
fd event_bus.py lib/crewai/src/crewai/events --exec cat -n {}

Repository: crewAIInc/crewAI

Length of output: 42854


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the async kickoff path and the exact code around aprepare_kickoff.
rg -n "async def aprepare_kickoff|def prepare_kickoff|akickoff|future\.result\(" lib/crewai/src/crewai -S

echo
echo "---- utils.py around the reviewed lines ----"
sed -n '390,450p' lib/crewai/src/crewai/crews/utils.py

echo
echo "---- kick off call sites / await handling ----"
rg -n "wrap_future\(|await .*future|future\.result\(" lib/crewai/src/crewai -S

Repository: crewAIInc/crewAI

Length of output: 8573


Avoid blocking aprepare_kickoff() on the event bus future. crewai_event_bus.emit(...) can return a future for async or threaded handlers, and calling future.result() here blocks Crew.akickoff()'s event loop until those listeners finish. If this path needs to wait, use an async wait such as await asyncio.wrap_future(future); otherwise, skip the synchronous wait in the async path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/crewai/src/crewai/crews/utils.py` around lines 423 - 431, The async
kickoff path in `aprepare_kickoff()` is blocking on the event bus future
returned by `crewai_event_bus.emit`, which can stall `Crew.akickoff()`. Update
the `CrewKickoffStartedEvent` handling branch to avoid calling `future.result()`
synchronously; either remove the wait in this async path or await the returned
future with an async-compatible mechanism such as `asyncio.wrap_future`. Refer
to `aprepare_kickoff`, `crewai_event_bus.emit`, and the
`CrewKickoffStartedEvent` branch when making the change.

Closes crewAIInc#6428

Adds a lightweight pre-inference layer that detects the language of an
agent system prompt and automatically translates it to the language the
target model handles best, before the prompt reaches the LLM.

New modules:
- utilities/model_profiles.py: Boundary-aware regex registry for 15
  model families (gpt-4o, claude, llama, qwen, deepseek, gemini, etc.)
- utilities/prompt_translator.py: Core pipeline — language detection via
  Unicode script analysis, token estimation, code block/URL/JSON
  extraction, translation via LLM, content-hash caching

Agent changes:
- Add auto_translate_prompt (default True) field to BaseAgent
- Add prompt_glossary field to BaseAgent for domain-term preservation
- Integration in Agent._build_execution_prompt() with try/except
  fail-safe, system-prompt-only scope

Design:
- System prompt only — user messages never translated
- Opt-out via Agent(auto_translate_prompt=False)
- Fail-safe: any exception logs and keeps original prompt
- Only cache successful translations (transient failures can retry)
- Zero new hard dependencies

Tests: 82 passing (model profiles + translator + integration).
ruff: all checks passed. mypy: no issues found.
@ashusnapx ashusnapx force-pushed the feat/language-aware-prompt-routing branch from 86cf68c to 47374ee Compare July 9, 2026 10:42
@ashusnapx ashusnapx changed the title feat: language-aware system prompt routing and async callback fix feat: add language-aware system prompt routing for multi-model agents Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Language-Aware System Prompt Routing for Multi-Model Agents

2 participants