mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-22 07:15:10 +00:00
The new edge-only hooks pages linked each other with versionless paths like `/learn/step-hooks`, which mintlify resolves against the frozen default version where those pages do not exist, breaking the CI link check. Uses the `/en/learn/...` form the other edge pages already use.
283 lines
9.1 KiB
Plaintext
283 lines
9.1 KiB
Plaintext
---
|
|
title: LLM Call Hooks
|
|
description: Learn how to use LLM call hooks to intercept, modify, and control language model interactions in CrewAI
|
|
mode: "wide"
|
|
---
|
|
|
|
LLM Call Hooks provide fine-grained control over language model interactions
|
|
during agent execution. These hooks allow you to intercept LLM calls, modify
|
|
prompts, transform responses, implement approval gates, and add custom logging
|
|
or monitoring.
|
|
|
|
## Overview
|
|
|
|
LLM hooks are executed at two interception points:
|
|
|
|
| Point | When | Hook receives |
|
|
|-------|------|---------------|
|
|
| `PRE_MODEL_CALL` | Before every LLM call | `LLMCallHookContext` |
|
|
| `POST_MODEL_CALL` | After every LLM call | `LLMCallHookContext` (with `response` set) |
|
|
|
|
Write them with the [`@on` decorator](/en/learn/execution-hooks). The
|
|
[legacy `@before_llm_call` / `@after_llm_call` decorators](#legacy-decorators)
|
|
keep working unchanged — both styles register on the same engine and run in one
|
|
ordered chain.
|
|
|
|
## Hook Signature
|
|
|
|
```python
|
|
from crewai.hooks import on, HookAborted, InterceptionPoint, LLMCallHookContext
|
|
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def before_hook(ctx: LLMCallHookContext) -> None:
|
|
# Mutate ctx.messages in place, or
|
|
# raise HookAborted(reason, source) to block the call
|
|
...
|
|
|
|
@on(InterceptionPoint.POST_MODEL_CALL)
|
|
def after_hook(ctx: LLMCallHookContext) -> str | None:
|
|
# Return a string to replace ctx.response
|
|
# Return None to keep the original response
|
|
...
|
|
```
|
|
|
|
Unlike the boundary and step points, the model-call points pass the rich
|
|
`LLMCallHookContext` directly as the hook argument (there is no separate
|
|
`ctx.payload`): mutate `ctx.messages` in place before the call, and return a
|
|
string to replace the response after it.
|
|
|
|
Blocking a call raises `ValueError("LLM call blocked by before_llm_call hook")`
|
|
inside the executor; the `HookAborted` reason and source are recorded in
|
|
[telemetry](/en/learn/execution-hooks#telemetry).
|
|
|
|
## LLM Hook Context
|
|
|
|
The `LLMCallHookContext` object provides comprehensive access to execution state:
|
|
|
|
```python
|
|
class LLMCallHookContext:
|
|
executor: CrewAgentExecutor | LiteAgent | None # Executor (None for direct LLM calls)
|
|
messages: list # Mutable message list
|
|
agent: Agent | None # Current agent (None for direct LLM calls)
|
|
task: Task | None # Current task (None for direct calls or LiteAgent)
|
|
crew: Crew | None # Crew instance (None for direct calls or LiteAgent)
|
|
llm: BaseLLM | None # LLM instance
|
|
iterations: int # Current iteration count (0 for direct calls)
|
|
response: str | None # LLM response (POST_MODEL_CALL only)
|
|
```
|
|
|
|
The context also exposes `request_human_input(prompt, default_message)`, which
|
|
pauses live console updates and collects input from the terminal — useful for
|
|
approval gates.
|
|
|
|
### Modifying Messages
|
|
|
|
**Important:** Always modify messages in-place:
|
|
|
|
```python
|
|
# ✅ Correct - modify in-place
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def add_context(ctx: LLMCallHookContext) -> None:
|
|
ctx.messages.append({"role": "system", "content": "Be concise"})
|
|
|
|
# ❌ Wrong - replaces list reference and breaks the executor
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def wrong_approach(ctx: LLMCallHookContext) -> None:
|
|
ctx.messages = [{"role": "system", "content": "Be concise"}]
|
|
```
|
|
|
|
## Registration Methods
|
|
|
|
### 1. Global Hooks
|
|
|
|
Apply to all LLM calls across all crews. Use the `agents=` filter to scope a
|
|
hook to specific agent roles:
|
|
|
|
```python
|
|
from crewai.hooks import on, InterceptionPoint
|
|
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def log_llm_call(ctx):
|
|
print(f"LLM call by {ctx.agent.role} at iteration {ctx.iterations}")
|
|
|
|
@on(InterceptionPoint.POST_MODEL_CALL, agents=["Researcher"])
|
|
def log_researcher_responses(ctx):
|
|
print(f"Response length: {len(ctx.response)}")
|
|
```
|
|
|
|
### 2. Crew-Scoped Hooks
|
|
|
|
Apply the same decorator to a method inside a `@CrewBase` class to scope the
|
|
hook to that crew only:
|
|
|
|
```python
|
|
from crewai.hooks import on, InterceptionPoint
|
|
|
|
@CrewBase
|
|
class MyProjCrew:
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def validate_inputs(self, ctx):
|
|
# Only applies to this crew
|
|
if ctx.iterations == 0:
|
|
print(f"Starting task: {ctx.task.description}")
|
|
|
|
@crew
|
|
def crew(self) -> Crew:
|
|
return Crew(agents=self.agents, tasks=self.tasks, process=Process.sequential)
|
|
```
|
|
|
|
## Common Use Cases
|
|
|
|
### 1. Iteration Limiting
|
|
|
|
```python
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def limit_iterations(ctx: LLMCallHookContext) -> None:
|
|
if ctx.iterations > 15:
|
|
raise HookAborted(reason="exceeded 15 iterations", source="loop-guard")
|
|
```
|
|
|
|
### 2. Human Approval Gate
|
|
|
|
```python
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def require_approval(ctx: LLMCallHookContext) -> None:
|
|
if ctx.iterations > 5:
|
|
response = ctx.request_human_input(
|
|
prompt=f"Iteration {ctx.iterations}: Approve LLM call?",
|
|
default_message="Press Enter to approve, or type 'no' to block:",
|
|
)
|
|
if response.lower() == "no":
|
|
raise HookAborted(reason="blocked by user", source="approval-gate")
|
|
```
|
|
|
|
### 3. Adding System Context
|
|
|
|
```python
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def add_guardrails(ctx: LLMCallHookContext) -> None:
|
|
ctx.messages.append({
|
|
"role": "system",
|
|
"content": "Ensure responses are factual and cite sources when possible."
|
|
})
|
|
```
|
|
|
|
### 4. Response Sanitization
|
|
|
|
```python
|
|
import re
|
|
|
|
@on(InterceptionPoint.POST_MODEL_CALL)
|
|
def sanitize_sensitive_data(ctx: LLMCallHookContext) -> str | None:
|
|
if not ctx.response:
|
|
return None
|
|
sanitized = re.sub(r'\b\d{3}-\d{2}-\d{4}\b', '[SSN-REDACTED]', ctx.response)
|
|
return re.sub(r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b', '[CARD-REDACTED]', sanitized)
|
|
```
|
|
|
|
### 5. Debug Logging
|
|
|
|
```python
|
|
@on(InterceptionPoint.PRE_MODEL_CALL)
|
|
def debug_request(ctx: LLMCallHookContext) -> None:
|
|
print(f"Agent: {ctx.agent.role}, iteration {ctx.iterations}, "
|
|
f"{len(ctx.messages)} messages")
|
|
|
|
@on(InterceptionPoint.POST_MODEL_CALL)
|
|
def debug_response(ctx: LLMCallHookContext) -> None:
|
|
if ctx.response:
|
|
print(f"Response preview: {ctx.response[:100]}...")
|
|
```
|
|
|
|
## Hook Management
|
|
|
|
```python
|
|
from crewai.hooks import (
|
|
InterceptionPoint,
|
|
clear_all_hooks,
|
|
clear_hooks,
|
|
get_hooks,
|
|
unregister_hook,
|
|
)
|
|
|
|
# Unregister a specific hook
|
|
unregister_hook(InterceptionPoint.PRE_MODEL_CALL, my_hook)
|
|
|
|
# Clear one point, or everything (e.g. between tests)
|
|
clear_hooks(InterceptionPoint.POST_MODEL_CALL)
|
|
clear_all_hooks()
|
|
|
|
# Inspect what's registered
|
|
print(len(get_hooks(InterceptionPoint.PRE_MODEL_CALL)))
|
|
```
|
|
|
|
The legacy management API (`register_before_llm_call_hook`,
|
|
`unregister_before_llm_call_hook`, `clear_before_llm_call_hooks`,
|
|
`clear_all_llm_call_hooks`, `get_before_llm_call_hooks`, and their `after_`
|
|
counterparts) operates on the same underlying registries, so either API can
|
|
manage hooks registered by the other.
|
|
|
|
## Legacy Decorators
|
|
|
|
The original per-point decorators keep working unchanged and run in the same
|
|
registration-order chain as `@on` hooks:
|
|
|
|
```python
|
|
from crewai.hooks import before_llm_call, after_llm_call
|
|
|
|
@before_llm_call
|
|
def validate_iteration_count(context):
|
|
if context.iterations > 10:
|
|
return False # Block execution
|
|
return None
|
|
|
|
@after_llm_call(agents=["Researcher"])
|
|
def sanitize_response(context):
|
|
if context.response and "API_KEY" in context.response:
|
|
return context.response.replace("API_KEY", "[REDACTED]")
|
|
return None
|
|
```
|
|
|
|
Differences from `@on`:
|
|
|
|
- **Blocking** is `return False` from a before hook — equivalent to raising
|
|
`HookAborted`, but without a custom reason or source for telemetry.
|
|
- **Signatures** are point-specific: before hooks return `bool | None`, after
|
|
hooks return `str | None`. The context object is the same
|
|
`LLMCallHookContext`.
|
|
- **Filters and crew-scoping** work the same way: `@before_llm_call(agents=[...])`,
|
|
and applying the decorator to a `@CrewBase` method scopes it to that crew.
|
|
|
|
Prefer `@on` for new code; keep the legacy style where it is already in use —
|
|
there is no behavioral penalty.
|
|
|
|
## Best Practices
|
|
|
|
1. **Keep hooks focused and fast** — they run on every LLM call
|
|
2. **Modify in-place** — always mutate `ctx.messages`, never replace the list
|
|
3. **Use type hints** — annotate with `LLMCallHookContext` for IDE support
|
|
4. **Abort loudly** — raise `HookAborted` with a meaningful reason and source;
|
|
any other exception is swallowed (fail-open)
|
|
5. **Clear hooks in tests** — call `clear_all_hooks()` between test runs
|
|
|
|
## Troubleshooting
|
|
|
|
### Hook Not Executing
|
|
- Verify the hook is registered before crew execution
|
|
- Check whether an earlier hook aborted (subsequent hooks don't run)
|
|
|
|
### Message Modifications Not Persisting
|
|
- Use in-place modifications: `ctx.messages.append(...)`
|
|
- Don't replace the list: `ctx.messages = []`
|
|
|
|
### Response Modifications Not Working
|
|
- Return the modified string from a `POST_MODEL_CALL` hook
|
|
- Returning `None` keeps the original response
|
|
|
|
## Related Documentation
|
|
|
|
- [Execution Hooks Overview →](/en/learn/execution-hooks)
|
|
- [Tool Call Hooks →](/en/learn/tool-hooks)
|
|
- [Execution Boundary Hooks →](/en/learn/execution-boundary-hooks)
|
|
- [Step Hooks →](/en/learn/step-hooks)
|