fix(llm): LLM antwortet jetzt in der konfigurierten Systemsprache
Bisher wurde `language` zwar an STT/TTS übergeben, aber nie an den LLM.
Lokales Modell antwortete deshalb immer auf Englisch/Deutsch, egal ob
Französisch, Spanisch o.a. eingestellt war.
Lösung: `language`-Parameter durch die gesamte LLM-Schicht gezogen:
- base.py: `lang_instruction()` helper + Signatur erweitert
- local_openai_compatible.py: Sprachanweisung ("Respond in Français.")
wird als letzter System-Part in den Message-Stack eingefügt
- openrouter.py: Explizite Sprachanweisung ergänzt den bestehenden
"Answer in the same language as the user"-Prompt
- fallback.py: FallbackLLMProvider leitet `language` durch
- orchestrator.py: alle 3 LLM-Aufrufstellen übergeben `language`
- Tests: alle Stub-LLMs um `language=None` ergänzt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c3f6206d0c
commit
81d4cf6fd8
14 changed files with 89 additions and 44 deletions
|
|
@ -99,6 +99,7 @@ class Orchestrator:
|
||||||
trace.semantic_response = await self.llm.complete(
|
trace.semantic_response = await self.llm.complete(
|
||||||
trace.cleaned_transcript or "",
|
trace.cleaned_transcript or "",
|
||||||
history=history,
|
history=history,
|
||||||
|
language=language,
|
||||||
)
|
)
|
||||||
if not trace.semantic_response:
|
if not trace.semantic_response:
|
||||||
raise RuntimeError("LLM returned an empty response")
|
raise RuntimeError("LLM returned an empty response")
|
||||||
|
|
@ -158,7 +159,7 @@ class Orchestrator:
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
stream_fn = getattr(self.llm, "stream", None)
|
stream_fn = getattr(self.llm, "stream", None)
|
||||||
if stream_fn is not None:
|
if stream_fn is not None:
|
||||||
async for delta in stream_fn(trace.cleaned_transcript or "", history=history):
|
async for delta in stream_fn(trace.cleaned_transcript or "", history=history, language=language):
|
||||||
parts.append(delta)
|
parts.append(delta)
|
||||||
if on_token:
|
if on_token:
|
||||||
await on_token(delta)
|
await on_token(delta)
|
||||||
|
|
@ -167,7 +168,7 @@ class Orchestrator:
|
||||||
await _emit_sentence(sentence)
|
await _emit_sentence(sentence)
|
||||||
else:
|
else:
|
||||||
# Provider ohne Streaming -> komplette Antwort als ein Token.
|
# Provider ohne Streaming -> komplette Antwort als ein Token.
|
||||||
result = await self.llm.complete(trace.cleaned_transcript or "", history=history)
|
result = await self.llm.complete(trace.cleaned_transcript or "", history=history, language=language)
|
||||||
parts.append(result)
|
parts.append(result)
|
||||||
if on_token:
|
if on_token:
|
||||||
await on_token(result)
|
await on_token(result)
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,13 @@ class FallbackSTTProvider(_Chain):
|
||||||
|
|
||||||
|
|
||||||
class FallbackLLMProvider(_Chain):
|
class FallbackLLMProvider(_Chain):
|
||||||
async def complete(self, text, history=None, session_id=None) -> str:
|
async def complete(self, text, history=None, session_id=None, language=None) -> str:
|
||||||
last_exc = None
|
last_exc = None
|
||||||
for index, (name, provider) in enumerate(self.entries):
|
for index, (name, provider) in enumerate(self.entries):
|
||||||
try:
|
try:
|
||||||
result = await provider.complete(text, history=history, session_id=session_id)
|
result = await provider.complete(
|
||||||
|
text, history=history, session_id=session_id, language=language
|
||||||
|
)
|
||||||
if index > 0:
|
if index > 0:
|
||||||
self._on_fallback()
|
self._on_fallback()
|
||||||
return result
|
return result
|
||||||
|
|
@ -50,12 +52,14 @@ class FallbackLLMProvider(_Chain):
|
||||||
self._on_error(name)
|
self._on_error(name)
|
||||||
raise last_exc
|
raise last_exc
|
||||||
|
|
||||||
async def stream(self, text, history=None, session_id=None) -> AsyncIterator[str]:
|
async def stream(self, text, history=None, session_id=None, language=None) -> AsyncIterator[str]:
|
||||||
last_exc = None
|
last_exc = None
|
||||||
for index, (name, provider) in enumerate(self.entries):
|
for index, (name, provider) in enumerate(self.entries):
|
||||||
produced = False
|
produced = False
|
||||||
try:
|
try:
|
||||||
async for delta in provider.stream(text, history=history, session_id=session_id):
|
async for delta in provider.stream(
|
||||||
|
text, history=history, session_id=session_id, language=language
|
||||||
|
):
|
||||||
produced = True
|
produced = True
|
||||||
yield delta
|
yield delta
|
||||||
if index > 0:
|
if index > 0:
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,26 @@ import json
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
_LANG_NAMES: dict[str, str] = {
|
||||||
|
"de": "Deutsch",
|
||||||
|
"en": "English",
|
||||||
|
"fr": "Français",
|
||||||
|
"es": "Español",
|
||||||
|
"it": "Italiano",
|
||||||
|
"nl": "Nederlands",
|
||||||
|
"ru": "Русский",
|
||||||
|
"zh": "中文",
|
||||||
|
"cmn": "中文",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def lang_instruction(language: str | None) -> str | None:
|
||||||
|
"""'Respond in X.' instruction for the given ISO language code, or None."""
|
||||||
|
if not language:
|
||||||
|
return None
|
||||||
|
name = _LANG_NAMES.get(language, language)
|
||||||
|
return f"Respond in {name}."
|
||||||
|
|
||||||
|
|
||||||
def sse_delta(line: str) -> str | None:
|
def sse_delta(line: str) -> str | None:
|
||||||
"""Extrahiert das Token-Delta aus einer OpenAI-kompatiblen SSE-Zeile (oder None)."""
|
"""Extrahiert das Token-Delta aus einer OpenAI-kompatiblen SSE-Zeile (oder None)."""
|
||||||
|
|
@ -24,6 +44,7 @@ class LLMProvider(ABC):
|
||||||
text: str,
|
text: str,
|
||||||
history: list[dict] | None = None,
|
history: list[dict] | None = None,
|
||||||
session_id: str | None = None,
|
session_id: str | None = None,
|
||||||
|
language: str | None = None,
|
||||||
) -> str: ...
|
) -> str: ...
|
||||||
|
|
||||||
async def stream(
|
async def stream(
|
||||||
|
|
@ -31,9 +52,10 @@ class LLMProvider(ABC):
|
||||||
text: str,
|
text: str,
|
||||||
history: list[dict] | None = None,
|
history: list[dict] | None = None,
|
||||||
session_id: str | None = None,
|
session_id: str | None = None,
|
||||||
|
language: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
"""Token-Stream. Default: kein echtes Streaming -> komplette Antwort als ein Chunk.
|
"""Token-Stream. Default: kein echtes Streaming -> komplette Antwort als ein Chunk.
|
||||||
|
|
||||||
Provider mit SSE-Unterstuetzung ueberschreiben diese Methode.
|
Provider mit SSE-Unterstuetzung ueberschreiben diese Methode.
|
||||||
"""
|
"""
|
||||||
yield await self.complete(text, history=history, session_id=session_id)
|
yield await self.complete(text, history=history, session_id=session_id, language=language)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from collections.abc import AsyncIterator
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from app.providers.llm.base import LLMProvider, sse_delta
|
from app.providers.llm.base import LLMProvider, lang_instruction, sse_delta
|
||||||
|
|
||||||
|
|
||||||
class LocalOpenAICompatibleLLM(LLMProvider):
|
class LocalOpenAICompatibleLLM(LLMProvider):
|
||||||
|
|
@ -24,7 +24,9 @@ class LocalOpenAICompatibleLLM(LLMProvider):
|
||||||
self.max_tokens = max_tokens
|
self.max_tokens = max_tokens
|
||||||
self.temperature = temperature
|
self.temperature = temperature
|
||||||
|
|
||||||
def _build_messages(self, text: str, history: list[dict] | None) -> list[dict]:
|
def _build_messages(
|
||||||
|
self, text: str, history: list[dict] | None, language: str | None = None
|
||||||
|
) -> list[dict]:
|
||||||
# Manche Chat-Templates (z. B. Qwen3) erlauben nur EINE System-Nachricht,
|
# Manche Chat-Templates (z. B. Qwen3) erlauben nur EINE System-Nachricht,
|
||||||
# ganz am Anfang. Daher den Sprach-System-Prompt und etwaige System-
|
# ganz am Anfang. Daher den Sprach-System-Prompt und etwaige System-
|
||||||
# Nachrichten aus der History (z. B. Nutzer-Erinnerungen) zu einer einzigen
|
# Nachrichten aus der History (z. B. Nutzer-Erinnerungen) zu einer einzigen
|
||||||
|
|
@ -40,6 +42,9 @@ class LocalOpenAICompatibleLLM(LLMProvider):
|
||||||
system_parts.append(content)
|
system_parts.append(content)
|
||||||
else:
|
else:
|
||||||
rest.append(msg)
|
rest.append(msg)
|
||||||
|
instr = lang_instruction(language)
|
||||||
|
if instr:
|
||||||
|
system_parts.append(instr)
|
||||||
|
|
||||||
messages: list[dict] = []
|
messages: list[dict] = []
|
||||||
if system_parts:
|
if system_parts:
|
||||||
|
|
@ -48,10 +53,12 @@ class LocalOpenAICompatibleLLM(LLMProvider):
|
||||||
messages.append({"role": "user", "content": text})
|
messages.append({"role": "user", "content": text})
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
def _payload(self, text: str, history: list[dict] | None, stream: bool) -> dict:
|
def _payload(
|
||||||
|
self, text: str, history: list[dict] | None, stream: bool, language: str | None = None
|
||||||
|
) -> dict:
|
||||||
payload: dict = {
|
payload: dict = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": self._build_messages(text, history),
|
"messages": self._build_messages(text, history, language=language),
|
||||||
"temperature": self.temperature,
|
"temperature": self.temperature,
|
||||||
}
|
}
|
||||||
if stream:
|
if stream:
|
||||||
|
|
@ -68,12 +75,13 @@ class LocalOpenAICompatibleLLM(LLMProvider):
|
||||||
text: str,
|
text: str,
|
||||||
history: list[dict] | None = None,
|
history: list[dict] | None = None,
|
||||||
session_id: str | None = None,
|
session_id: str | None = None,
|
||||||
|
language: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
async with httpx.AsyncClient(timeout=120) as client:
|
async with httpx.AsyncClient(timeout=120) as client:
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"{self.base_url}/chat/completions",
|
f"{self.base_url}/chat/completions",
|
||||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||||
json=self._payload(text, history, stream=False),
|
json=self._payload(text, history, stream=False, language=language),
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
@ -84,13 +92,14 @@ class LocalOpenAICompatibleLLM(LLMProvider):
|
||||||
text: str,
|
text: str,
|
||||||
history: list[dict] | None = None,
|
history: list[dict] | None = None,
|
||||||
session_id: str | None = None,
|
session_id: str | None = None,
|
||||||
|
language: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
async with httpx.AsyncClient(timeout=120) as client:
|
async with httpx.AsyncClient(timeout=120) as client:
|
||||||
async with client.stream(
|
async with client.stream(
|
||||||
"POST",
|
"POST",
|
||||||
f"{self.base_url}/chat/completions",
|
f"{self.base_url}/chat/completions",
|
||||||
headers={"Authorization": f"Bearer {self.api_key}"},
|
headers={"Authorization": f"Bearer {self.api_key}"},
|
||||||
json=self._payload(text, history, stream=True),
|
json=self._payload(text, history, stream=True, language=language),
|
||||||
) as response:
|
) as response:
|
||||||
if response.status_code >= 400:
|
if response.status_code >= 400:
|
||||||
body = await response.aread()
|
body = await response.aread()
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from collections.abc import AsyncIterator
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from app.providers.llm.base import LLMProvider, sse_delta
|
from app.providers.llm.base import LLMProvider, lang_instruction, sse_delta
|
||||||
|
|
||||||
|
|
||||||
SYSTEM_PROMPT = """
|
SYSTEM_PROMPT = """
|
||||||
|
|
@ -51,7 +51,9 @@ class OpenRouterLLMProvider(LLMProvider):
|
||||||
self.api_key = (api_key or "").strip()
|
self.api_key = (api_key or "").strip()
|
||||||
self.model = (model or "").strip()
|
self.model = (model or "").strip()
|
||||||
|
|
||||||
def _build_messages(self, text: str, history: list[dict] | None) -> list[dict]:
|
def _build_messages(
|
||||||
|
self, text: str, history: list[dict] | None, language: str | None = None
|
||||||
|
) -> list[dict]:
|
||||||
if not self.api_key:
|
if not self.api_key:
|
||||||
raise ValueError("OPENROUTER_API_KEY is empty")
|
raise ValueError("OPENROUTER_API_KEY is empty")
|
||||||
if not self.model:
|
if not self.model:
|
||||||
|
|
@ -59,7 +61,12 @@ class OpenRouterLLMProvider(LLMProvider):
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
raise ValueError("LLM input text is empty")
|
raise ValueError("LLM input text is empty")
|
||||||
|
|
||||||
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
|
system_content = SYSTEM_PROMPT
|
||||||
|
instr = lang_instruction(language)
|
||||||
|
if instr:
|
||||||
|
system_content = f"{SYSTEM_PROMPT}\n\n{instr}"
|
||||||
|
|
||||||
|
messages = [{"role": "system", "content": system_content}]
|
||||||
if history:
|
if history:
|
||||||
messages.extend(history)
|
messages.extend(history)
|
||||||
messages.append({"role": "user", "content": text.strip()})
|
messages.append({"role": "user", "content": text.strip()})
|
||||||
|
|
@ -70,10 +77,11 @@ class OpenRouterLLMProvider(LLMProvider):
|
||||||
text: str,
|
text: str,
|
||||||
history: list[dict] | None = None,
|
history: list[dict] | None = None,
|
||||||
session_id: str | None = None,
|
session_id: str | None = None,
|
||||||
|
language: str | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": self._build_messages(text, history),
|
"messages": self._build_messages(text, history, language=language),
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0)
|
timeout = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0)
|
||||||
|
|
@ -115,10 +123,11 @@ class OpenRouterLLMProvider(LLMProvider):
|
||||||
text: str,
|
text: str,
|
||||||
history: list[dict] | None = None,
|
history: list[dict] | None = None,
|
||||||
session_id: str | None = None,
|
session_id: str | None = None,
|
||||||
|
language: str | None = None,
|
||||||
) -> AsyncIterator[str]:
|
) -> AsyncIterator[str]:
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": self._build_messages(text, history),
|
"messages": self._build_messages(text, history, language=language),
|
||||||
"stream": True,
|
"stream": True,
|
||||||
}
|
}
|
||||||
timeout = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0)
|
timeout = httpx.Timeout(connect=10.0, read=120.0, write=30.0, pool=10.0)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class StubLLM:
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
self.calls = 0
|
self.calls = 0
|
||||||
|
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
self.calls += 1
|
self.calls += 1
|
||||||
return self.raw
|
return self.raw
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ def test_session_route_applies(monkeypatch):
|
||||||
|
|
||||||
def test_chat_per_request_override_and_loopback(monkeypatch):
|
def test_chat_per_request_override_and_loopback(monkeypatch):
|
||||||
class StubLLM:
|
class StubLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "Mir geht es gut, danke."
|
return "Mir geht es gut, danke."
|
||||||
|
|
||||||
class StubTTS:
|
class StubTTS:
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ client = TestClient(app)
|
||||||
|
|
||||||
def _install_stubs(monkeypatch, captured):
|
def _install_stubs(monkeypatch, captured):
|
||||||
class MemLLM:
|
class MemLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
captured["history"] = list(history or [])
|
captured["history"] = list(history or [])
|
||||||
return f"Antwort auf: {text}"
|
return f"Antwort auf: {text}"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ class StubLLM:
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
self.calls = 0
|
self.calls = 0
|
||||||
|
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
self.calls += 1
|
self.calls += 1
|
||||||
return self.raw
|
return self.raw
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ client = TestClient(app)
|
||||||
|
|
||||||
def _stub_chat(monkeypatch, answer="ok"):
|
def _stub_chat(monkeypatch, answer="ok"):
|
||||||
class StubLLM:
|
class StubLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return answer
|
return answer
|
||||||
|
|
||||||
class StubTTS:
|
class StubTTS:
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,10 @@ def test_energy_vad_ignores_silence_without_speech():
|
||||||
|
|
||||||
def _install_slow_stream(monkeypatch):
|
def _install_slow_stream(monkeypatch):
|
||||||
class SlowLLM:
|
class SlowLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "fertig"
|
return "fertig"
|
||||||
|
|
||||||
async def stream(self, text, history=None, session_id=None):
|
async def stream(self, text, history=None, session_id=None, language=None):
|
||||||
for i in range(200):
|
for i in range(200):
|
||||||
await asyncio.sleep(0.005)
|
await asyncio.sleep(0.005)
|
||||||
yield f"t{i} "
|
yield f"t{i} "
|
||||||
|
|
@ -89,7 +89,7 @@ def _install_voice_stubs(monkeypatch):
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
class LLM:
|
class LLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "antwort"
|
return "antwort"
|
||||||
|
|
||||||
class TTS:
|
class TTS:
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,11 @@ def _run(coro):
|
||||||
|
|
||||||
def test_llm_fallback_uses_second_on_error():
|
def test_llm_fallback_uses_second_on_error():
|
||||||
class BadLLM:
|
class BadLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
raise RuntimeError("down")
|
raise RuntimeError("down")
|
||||||
|
|
||||||
class GoodLLM:
|
class GoodLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
chain = FallbackLLMProvider("llm", [("bad", BadLLM()), ("good", GoodLLM())])
|
chain = FallbackLLMProvider("llm", [("bad", BadLLM()), ("good", GoodLLM())])
|
||||||
|
|
@ -37,7 +37,7 @@ def test_llm_fallback_uses_second_on_error():
|
||||||
|
|
||||||
def test_llm_fallback_all_fail_raises():
|
def test_llm_fallback_all_fail_raises():
|
||||||
class BadLLM:
|
class BadLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
raise RuntimeError("x")
|
raise RuntimeError("x")
|
||||||
|
|
||||||
chain = FallbackLLMProvider("llm", [("a", BadLLM()), ("b", BadLLM())])
|
chain = FallbackLLMProvider("llm", [("a", BadLLM()), ("b", BadLLM())])
|
||||||
|
|
@ -47,18 +47,18 @@ def test_llm_fallback_all_fail_raises():
|
||||||
|
|
||||||
def test_llm_stream_fallback_before_first_token():
|
def test_llm_stream_fallback_before_first_token():
|
||||||
class BadStream:
|
class BadStream:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "x"
|
return "x"
|
||||||
|
|
||||||
async def stream(self, text, history=None, session_id=None):
|
async def stream(self, text, history=None, session_id=None, language=None):
|
||||||
raise RuntimeError("boom")
|
raise RuntimeError("boom")
|
||||||
yield # macht die Funktion zum Generator
|
yield # macht die Funktion zum Generator
|
||||||
|
|
||||||
class GoodStream:
|
class GoodStream:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "ok"
|
return "ok"
|
||||||
|
|
||||||
async def stream(self, text, history=None, session_id=None):
|
async def stream(self, text, history=None, session_id=None, language=None):
|
||||||
yield "he"
|
yield "he"
|
||||||
yield "llo"
|
yield "llo"
|
||||||
|
|
||||||
|
|
@ -74,11 +74,11 @@ def test_llm_stream_fallback_before_first_token():
|
||||||
|
|
||||||
def test_config_llm_fallback_applied(monkeypatch):
|
def test_config_llm_fallback_applied(monkeypatch):
|
||||||
class BadLLM:
|
class BadLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
raise RuntimeError("primary down")
|
raise RuntimeError("primary down")
|
||||||
|
|
||||||
class GoodLLM:
|
class GoodLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "rescued"
|
return "rescued"
|
||||||
|
|
||||||
class StubTTS:
|
class StubTTS:
|
||||||
|
|
@ -102,7 +102,7 @@ def test_config_llm_fallback_applied(monkeypatch):
|
||||||
|
|
||||||
def test_metrics_endpoint_records_requests_and_stages(monkeypatch):
|
def test_metrics_endpoint_records_requests_and_stages(monkeypatch):
|
||||||
class StubLLM:
|
class StubLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "hi"
|
return "hi"
|
||||||
|
|
||||||
class StubTTS:
|
class StubTTS:
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ def test_sse_delta_parsing():
|
||||||
|
|
||||||
def test_base_stream_default_yields_full_completion():
|
def test_base_stream_default_yields_full_completion():
|
||||||
class P(LLMProvider):
|
class P(LLMProvider):
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "ganze Antwort"
|
return "ganze Antwort"
|
||||||
|
|
||||||
async def run():
|
async def run():
|
||||||
|
|
@ -51,10 +51,10 @@ def test_base_stream_default_yields_full_completion():
|
||||||
|
|
||||||
def _install_streaming(monkeypatch, tokens):
|
def _install_streaming(monkeypatch, tokens):
|
||||||
class StreamLLM:
|
class StreamLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "".join(tokens)
|
return "".join(tokens)
|
||||||
|
|
||||||
async def stream(self, text, history=None, session_id=None):
|
async def stream(self, text, history=None, session_id=None, language=None):
|
||||||
for tok in tokens:
|
for tok in tokens:
|
||||||
yield tok
|
yield tok
|
||||||
|
|
||||||
|
|
@ -95,7 +95,7 @@ def test_ws_without_stream_flag_has_no_tokens(monkeypatch):
|
||||||
|
|
||||||
def test_ws_stream_fallback_for_nonstreaming_llm(monkeypatch):
|
def test_ws_stream_fallback_for_nonstreaming_llm(monkeypatch):
|
||||||
class OnlyComplete:
|
class OnlyComplete:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "komplett"
|
return "komplett"
|
||||||
|
|
||||||
class StubTTS:
|
class StubTTS:
|
||||||
|
|
@ -118,10 +118,10 @@ def test_ws_audio_stream_sends_chunks_per_sentence(monkeypatch):
|
||||||
tts_calls = []
|
tts_calls = []
|
||||||
|
|
||||||
class StreamLLM:
|
class StreamLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return "Satz eins. Satz zwei."
|
return "Satz eins. Satz zwei."
|
||||||
|
|
||||||
async def stream(self, text, history=None, session_id=None):
|
async def stream(self, text, history=None, session_id=None, language=None):
|
||||||
for tok in ["Satz ", "eins. ", "Satz ", "zwei."]:
|
for tok in ["Satz ", "eins. ", "Satz ", "zwei."]:
|
||||||
yield tok
|
yield tok
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ client = TestClient(app)
|
||||||
|
|
||||||
def _install_stubs(monkeypatch, captured):
|
def _install_stubs(monkeypatch, captured):
|
||||||
class MemLLM:
|
class MemLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
captured["history"] = list(history or [])
|
captured["history"] = list(history or [])
|
||||||
return f"Echo: {text}"
|
return f"Echo: {text}"
|
||||||
|
|
||||||
|
|
@ -74,7 +74,7 @@ def _install_voice_stubs(monkeypatch):
|
||||||
return f"erkannt({len(audio_bytes)})"
|
return f"erkannt({len(audio_bytes)})"
|
||||||
|
|
||||||
class StubLLM:
|
class StubLLM:
|
||||||
async def complete(self, text, history=None, session_id=None):
|
async def complete(self, text, history=None, session_id=None, language=None):
|
||||||
return f"Antwort zu {text}"
|
return f"Antwort zu {text}"
|
||||||
|
|
||||||
class StubTTS:
|
class StubTTS:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue