Files
ragflow/test/unit_test/agent/component/test_llm_prompt.py
Taranum Wasu e23f63bd93 fix(agent): prevent empty LLM user message after prompt fitting (#16413)
## Summary
- Treat `max_tokens=0` as unset (`or 8192`) when building model context
budgets, fixing agents that silently zeroed prompts when a vLLM model
had `max_tokens: 0` in tenant config
- Replace trailing same-role canvas history in `LLM._sys_prompt_and_msg`
instead of skipping the current user prompt
- Add `LLM.fit_messages()` validation after `message_fit_in` on agent
paths so empty user content fails fast with a clear error instead of
reaching vLLM

Fixes #16411

## Root cause
Agent canvas workflow called `message_fit_in` with `int(max_length *
0.97)`. When `max_length` was `0`, both system and user content were
trimmed to empty strings. The `[HISTORY STREAMLY]` log showing only
`{"role":"user","content":""}` matches this. A secondary bug skipped
appending the formatted user prompt when history ended with a `user`
role message.

## Test plan
- [x] Added `test/unit_test/agent/component/test_llm_prompt.py` for
role-replace, validation, and zero-budget fitting
- [x] Added
`test_message_fit_in_zero_budget_preserves_non_empty_messages` in
`test_generator_message_fit_in.py`
- [ ] CI unit tests
- [ ] Manual: agent canvas `begin → Retrieval → Agent → Message` with
vLLM Qwen3; confirm user message reaches LLM

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Taranum Wasu <taranumwasu@Taranums-MacBook-Pro.local>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 09:30:54 +08:00

62 lines
1.8 KiB
Python

import pytest
from agent.component.llm import LLM, LLMParam
def _llm(prompts=None):
cpn = LLM.__new__(LLM)
cpn._param = LLMParam()
cpn._param.prompts = prompts or [{"role": "user", "content": "User query: {sys.query}"}]
return cpn
@pytest.mark.p1
def test_sys_prompt_and_msg_replaces_trailing_user_instead_of_skipping():
cpn = _llm()
msg, _ = cpn._sys_prompt_and_msg([{"role": "user", "content": ""}], {"sys.query": "test"})
assert msg == [{"role": "user", "content": "User query: test"}]
@pytest.mark.p1
def test_sys_prompt_and_msg_keeps_consecutive_configured_prompts():
cpn = _llm(
[
{"role": "user", "content": "Context: {sys.query}"},
{"role": "user", "content": "User query: {sys.query}"},
]
)
msg, _ = cpn._sys_prompt_and_msg([], {"sys.query": "test"})
assert msg == [
{"role": "user", "content": "Context: test"},
{"role": "user", "content": "User query: test"},
]
@pytest.mark.p1
def test_validate_fitted_messages_requires_trailing_user():
err = LLM.validate_fitted_messages(
[
{"role": "system", "content": "system"},
{"role": "user", "content": "still here"},
{"role": "assistant", "content": "reply"},
]
)
assert err and "empty" in err.lower()
@pytest.mark.p1
def test_validate_fitted_messages_rejects_empty_user():
err = LLM.validate_fitted_messages([{"role": "system", "content": "system"}, {"role": "user", "content": ""}])
assert err and "empty" in err.lower()
@pytest.mark.p1
def test_fit_messages_uses_default_context_when_max_length_zero():
msg_fit, err = LLM.fit_messages(
"s" * 845,
[{"role": "user", "content": "User query: test"}],
0,
)
assert err is None
assert msg_fit[-1]["content"] == "User query: test"