mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
fix: merge system messages and place them first to comply with Qwen/v… (#39136)
This commit is contained in:
parent
7d635d9f7e
commit
34fdac2113
@ -304,16 +304,40 @@ def _map_messages_to_prompt_messages(
|
||||
for part in (Model._get_instruction_parts(messages, model_request_parameters) or [])
|
||||
if part.content.strip()
|
||||
]
|
||||
if instruction_messages:
|
||||
insert_at = next(
|
||||
(index for index, message in enumerate(prompt_messages) if not isinstance(message, SystemPromptMessage)),
|
||||
len(prompt_messages),
|
||||
)
|
||||
prompt_messages[insert_at:insert_at] = instruction_messages
|
||||
prompt_messages = _order_system_messages_first(prompt_messages, instruction_messages)
|
||||
|
||||
return prompt_messages
|
||||
|
||||
|
||||
def _order_system_messages_first(
|
||||
prompt_messages: Sequence[PromptMessage],
|
||||
instruction_messages: Sequence[SystemPromptMessage],
|
||||
) -> list[PromptMessage]:
|
||||
"""Merge all system content into a single leading system message.
|
||||
|
||||
Some providers (e.g. vLLM serving Qwen3.5/3.6 chat templates) reject any
|
||||
system message that is not exactly the first message, so sorting alone is
|
||||
not enough: multiple system messages must be merged into one.
|
||||
"""
|
||||
system_contents: list[str] = []
|
||||
non_system_messages: list[PromptMessage] = []
|
||||
for message in prompt_messages:
|
||||
if isinstance(message, SystemPromptMessage):
|
||||
text = message.get_text_content()
|
||||
if text.strip():
|
||||
system_contents.append(text)
|
||||
else:
|
||||
non_system_messages.append(message)
|
||||
for instruction in instruction_messages:
|
||||
text = instruction.get_text_content()
|
||||
if text.strip():
|
||||
system_contents.append(text)
|
||||
|
||||
if not system_contents:
|
||||
return non_system_messages
|
||||
return [SystemPromptMessage(content="\n\n".join(system_contents)), *non_system_messages]
|
||||
|
||||
|
||||
def _map_model_request_to_prompt_messages(message: ModelRequest) -> list[PromptMessage]:
|
||||
prompt_messages: list[PromptMessage] = []
|
||||
|
||||
|
||||
@ -171,12 +171,11 @@ class DifyLLMAdapterModelTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(set(tools_by_name), {"weather", "incident_summary"})
|
||||
self.assertEqual(tools_by_name["incident_summary"]["parameters"]["required"], ["title"])
|
||||
self.assertEqual(data["prompt_messages"][0]["role"], "system")
|
||||
self.assertEqual(data["prompt_messages"][0]["content"], "request system")
|
||||
self.assertEqual(data["prompt_messages"][1]["content"], "be concise")
|
||||
self.assertEqual(data["prompt_messages"][2]["content"], "hello")
|
||||
self.assertEqual(data["prompt_messages"][0]["content"], "request system\n\nbe concise")
|
||||
self.assertEqual(data["prompt_messages"][1]["content"], "hello")
|
||||
self.assertEqual(data["prompt_messages"][2]["role"], "tool")
|
||||
self.assertEqual(data["prompt_messages"][3]["role"], "tool")
|
||||
self.assertEqual(data["prompt_messages"][4]["role"], "tool")
|
||||
self.assertEqual(data["prompt_messages"][5]["role"], "assistant")
|
||||
self.assertEqual(data["prompt_messages"][4]["role"], "assistant")
|
||||
return build_stream_response(
|
||||
LLMResultChunk(
|
||||
model="demo-model",
|
||||
@ -291,6 +290,78 @@ class DifyLLMAdapterModelTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(usage.time_to_first_token, 0.2)
|
||||
self.assertEqual(usage.time_to_generate, 0.6)
|
||||
|
||||
async def test_request_merges_system_messages_before_history(self) -> None:
|
||||
messages = [
|
||||
ModelRequest(parts=[UserPromptPart("previous user")]),
|
||||
ModelResponse(parts=[TextPart(content="previous answer")]),
|
||||
ModelRequest(parts=[SystemPromptPart("current system"), UserPromptPart("current user")]),
|
||||
]
|
||||
request_parameters = ModelRequestParameters(instruction_parts=[InstructionPart(content="runtime instruction")])
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
payload = json.loads(request.content.decode("utf-8"))
|
||||
prompt_messages = payload["target"]["prompt_messages"]
|
||||
|
||||
self.assertEqual(
|
||||
[message["role"] for message in prompt_messages],
|
||||
["system", "user", "assistant", "user"],
|
||||
)
|
||||
self.assertEqual(prompt_messages[0]["content"], "current system\n\nruntime instruction")
|
||||
self.assertEqual(prompt_messages[1]["content"], "previous user")
|
||||
self.assertEqual(prompt_messages[2]["content"], "previous answer")
|
||||
self.assertEqual(prompt_messages[3]["content"], "current user")
|
||||
return build_stream_response(*single_text_chunk("adapter response", prompt_tokens=11, completion_tokens=7))
|
||||
|
||||
async with self.mock_gateway_stream(httpx.MockTransport(handler)):
|
||||
adapter = DifyLLMAdapterModel(
|
||||
"demo-model",
|
||||
self.make_provider(),
|
||||
model_provider="openai",
|
||||
)
|
||||
|
||||
response = await adapter.request(
|
||||
messages,
|
||||
model_settings=None,
|
||||
model_request_parameters=request_parameters,
|
||||
)
|
||||
|
||||
self.assertEqual(response.model_name, "demo-model")
|
||||
self.assertEqual(cast(TextPart, response.parts[0]).content, "adapter response")
|
||||
|
||||
async def test_request_merges_scattered_system_messages_without_instructions(self) -> None:
|
||||
messages = [
|
||||
ModelRequest(parts=[SystemPromptPart("first system"), UserPromptPart("hello")]),
|
||||
ModelResponse(parts=[TextPart(content="answer")]),
|
||||
ModelRequest(parts=[SystemPromptPart("second system"), UserPromptPart("follow up")]),
|
||||
]
|
||||
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
payload = json.loads(request.content.decode("utf-8"))
|
||||
prompt_messages = payload["target"]["prompt_messages"]
|
||||
|
||||
self.assertEqual(
|
||||
[message["role"] for message in prompt_messages],
|
||||
["system", "user", "assistant", "user"],
|
||||
)
|
||||
self.assertEqual(prompt_messages[0]["content"], "first system\n\nsecond system")
|
||||
return build_stream_response(*single_text_chunk("adapter response", prompt_tokens=11, completion_tokens=7))
|
||||
|
||||
async with self.mock_gateway_stream(httpx.MockTransport(handler)):
|
||||
adapter = DifyLLMAdapterModel(
|
||||
"demo-model",
|
||||
self.make_provider(),
|
||||
model_provider="openai",
|
||||
)
|
||||
|
||||
response = await adapter.request(
|
||||
messages,
|
||||
model_settings=None,
|
||||
model_request_parameters=ModelRequestParameters(),
|
||||
)
|
||||
|
||||
self.assertEqual(response.model_name, "demo-model")
|
||||
self.assertEqual(cast(TextPart, response.parts[0]).content, "adapter response")
|
||||
|
||||
async def test_request_maps_tool_call_only_assistant_history_to_empty_string_content(self) -> None:
|
||||
messages = [
|
||||
ModelRequest(parts=[SystemPromptPart("request system"), UserPromptPart("hello")]),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user