diff --git a/app/providers/llm/local_openai_compatible.py b/app/providers/llm/local_openai_compatible.py index 5985869..c550aed 100644 --- a/app/providers/llm/local_openai_compatible.py +++ b/app/providers/llm/local_openai_compatible.py @@ -25,13 +25,26 @@ class LocalOpenAICompatibleLLM(LLMProvider): self.temperature = temperature def _build_messages(self, text: str, history: list[dict] | None) -> list[dict]: - messages: list[dict] = [] - # Sprach-System-Prompt zuerst (knappe, vorlesbare Antworten). Etwaige - # System-Nachrichten aus der History (z. B. Nutzer-Erinnerungen) bleiben erhalten. + # Manche Chat-Templates (z. B. Qwen3) erlauben nur EINE System-Nachricht, + # ganz am Anfang. Daher den Sprach-System-Prompt und etwaige System- + # Nachrichten aus der History (z. B. Nutzer-Erinnerungen) zu einer einzigen + # fuehrenden System-Nachricht zusammenfuehren. + system_parts: list[str] = [] if self.system_prompt: - messages.append({"role": "system", "content": self.system_prompt}) - if history: - messages.extend(history) + system_parts.append(self.system_prompt) + rest: list[dict] = [] + for msg in history or []: + if msg.get("role") == "system": + content = (msg.get("content") or "").strip() + if content: + system_parts.append(content) + else: + rest.append(msg) + + messages: list[dict] = [] + if system_parts: + messages.append({"role": "system", "content": "\n\n".join(system_parts)}) + messages.extend(rest) messages.append({"role": "user", "content": text}) return messages diff --git a/tests/test_local_llm_messages.py b/tests/test_local_llm_messages.py new file mode 100644 index 0000000..4c5ea46 --- /dev/null +++ b/tests/test_local_llm_messages.py @@ -0,0 +1,30 @@ +from app.providers.llm.local_openai_compatible import LocalOpenAICompatibleLLM + + +def _llm(system_prompt="Sei kurz."): + return LocalOpenAICompatibleLLM("http://x/v1", "k", "m", system_prompt=system_prompt) + + +def test_single_system_message_when_history_has_system(): + llm = _llm() + history = [ + {"role": "system", "content": "Was du ueber den Nutzer weisst:\n- heisst Anna"}, + {"role": "user", "content": "Hallo"}, + {"role": "assistant", "content": "Hi Anna"}, + ] + msgs = llm._build_messages("Wie geht es dir?", history) + + # Genau EINE System-Nachricht, ganz am Anfang (Qwen3-Template-Anforderung). + assert sum(1 for m in msgs if m["role"] == "system") == 1 + assert msgs[0]["role"] == "system" + assert "Sei kurz." in msgs[0]["content"] + assert "heisst Anna" in msgs[0]["content"] + # Reihenfolge der Nicht-System-Nachrichten bleibt erhalten, User zuletzt. + assert [m["role"] for m in msgs] == ["system", "user", "assistant", "user"] + assert msgs[-1] == {"role": "user", "content": "Wie geht es dir?"} + + +def test_no_system_message_without_prompt_or_history(): + llm = _llm(system_prompt="") + msgs = llm._build_messages("Hallo", None) + assert msgs == [{"role": "user", "content": "Hallo"}]