| """Tests for the shared public chat runtime.""" |
|
|
| from __future__ import annotations |
|
|
| import pytest |
|
|
| from maris_core.space_chat import ( |
| DEFAULT_CHAT_MODEL, |
| SpaceChatRequest, |
| build_space_chat_messages, |
| generate_space_chat_reply, |
| resolve_space_chat_models, |
| ) |
| from maris_core.utils.hf_integration import HFIntegration |
|
|
|
|
| def test_space_chat_request_accepts_external_hf_model_ids() -> None: |
| request = SpaceChatRequest(message="Sveiki", model="Qwen/Qwen3-Coder-480B-A35B-Instruct") |
|
|
| assert request.model == "Qwen/Qwen3-Coder-480B-A35B-Instruct" |
|
|
|
|
| class _StopIterationChoices: |
| def __bool__(self) -> bool: |
| return True |
|
|
| def __getitem__(self, index: int) -> object: |
| raise StopIteration(index) |
|
|
| def __iter__(self): |
| return iter(()) |
|
|
|
|
| class _MalformedChatClient: |
| def __init__(self, token: str | None = None) -> None: |
| self.token = token |
| self.generation_models: list[str] = [] |
|
|
| def chat_completion(self, **_: object) -> dict[str, object]: |
| return {"choices": _StopIterationChoices()} |
|
|
| def text_generation(self, **kwargs: object) -> str: |
| self.generation_models.append(str(kwargs["model"])) |
| return "Fallback response from text_generation." |
|
|
|
|
| class _StopIterationChatClient: |
| def __init__(self, token: str | None = None) -> None: |
| self.token = token |
| self.generation_models: list[str] = [] |
|
|
| def chat_completion(self, **_: object) -> dict[str, object]: |
| return next(iter(())) |
|
|
| def text_generation(self, **kwargs: object) -> str: |
| self.generation_models.append(str(kwargs["model"])) |
| return "Fallback response after StopIteration." |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_generate_space_chat_reply_uses_hf_inference_space_runtime_config( |
| monkeypatch, |
| ) -> None: |
| captured_kwargs: dict[str, object] = {} |
|
|
| async def fake_save_conversation( |
| self, |
| prompt: str, |
| response: str, |
| metadata: dict[str, object] | None = None, |
| ) -> None: |
| del self, prompt, response, metadata |
|
|
| class _ConfiguredChatClient: |
| def __init__(self, **kwargs: object) -> None: |
| captured_kwargs.update(kwargs) |
|
|
| def chat_completion(self, **kwargs: object) -> dict[str, object]: |
| return { |
| "choices": [ |
| { |
| "message": { |
| "content": f"Atbilde no {kwargs['model']}", |
| } |
| } |
| ] |
| } |
|
|
| monkeypatch.setattr(HFIntegration, "save_conversation", fake_save_conversation) |
| monkeypatch.setenv("HF_INFERENCE_API_KEY", "hf_inference_secret") |
|
|
| response = await generate_space_chat_reply( |
| SpaceChatRequest(message="Palīdzi ar manu jautājumu"), |
| client_factory=_ConfiguredChatClient, |
| ) |
|
|
| assert response.model == DEFAULT_CHAT_MODEL |
| assert captured_kwargs == { |
| "provider": "hf-inference", |
| "base_url": "https://api-inference.huggingface.co", |
| "token": "hf_inference_secret", |
| } |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_generate_space_chat_reply_falls_back_on_malformed_chat_completion( |
| monkeypatch, |
| ) -> None: |
| async def fake_save_conversation( |
| self, |
| prompt: str, |
| response: str, |
| metadata: dict[str, object] | None = None, |
| ) -> None: |
| del self, prompt, response, metadata |
|
|
| monkeypatch.setattr(HFIntegration, "save_conversation", fake_save_conversation) |
|
|
| response = await generate_space_chat_reply( |
| SpaceChatRequest(message="Palīdzi ar manu jautājumu"), |
| client_factory=_MalformedChatClient, |
| ) |
|
|
| assert response.response == "Fallback response from text_generation." |
| assert response.model == DEFAULT_CHAT_MODEL |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_generate_space_chat_reply_falls_back_when_chat_completion_raises_stop_iteration( |
| monkeypatch, |
| ) -> None: |
| async def fake_save_conversation( |
| self, |
| prompt: str, |
| response: str, |
| metadata: dict[str, object] | None = None, |
| ) -> None: |
| del self, prompt, response, metadata |
|
|
| monkeypatch.setattr(HFIntegration, "save_conversation", fake_save_conversation) |
|
|
| response = await generate_space_chat_reply( |
| SpaceChatRequest(message="Palīdzi ar manu jautājumu"), |
| client_factory=_StopIterationChatClient, |
| ) |
|
|
| assert response.response == "Fallback response after StopIteration." |
| assert response.model == DEFAULT_CHAT_MODEL |
|
|
|
|
| def test_resolve_space_chat_models_appends_hidden_fallbacks(monkeypatch) -> None: |
| monkeypatch.setenv("MARIS_CHAT_MODEL", "MarisUK/maris-ai-text") |
| monkeypatch.setenv( |
| "MARIS_CHAT_FALLBACK_MODELS", |
| "MarisUK/maris-assistant-runtime-fallback,Qwen/Qwen3-Coder-480B-A35B-Instruct", |
| ) |
|
|
| models = resolve_space_chat_models("MarisUK/offline-model") |
|
|
| assert models == ( |
| "MarisUK/offline-model", |
| "MarisUK/maris-ai-text", |
| "MarisUK/maris-assistant-runtime-fallback", |
| "Qwen/Qwen3-Coder-480B-A35B-Instruct", |
| ) |
|
|
|
|
| def test_build_space_chat_messages_drops_pending_duplicate_user_turn() -> None: |
| messages = build_space_chat_messages( |
| SpaceChatRequest( |
| message="Palīdzi ar plānu", |
| history=[ |
| {"role": "user", "content": "Iepriekšējais jautājums"}, |
| {"role": "assistant", "content": "Iepriekšējā atbilde"}, |
| {"role": "user", "content": "Palīdzi ar plānu"}, |
| ], |
| ) |
| ) |
|
|
| assert messages[-3:] == [ |
| {"role": "user", "content": "Iepriekšējais jautājums"}, |
| {"role": "assistant", "content": "Iepriekšējā atbilde"}, |
| {"role": "user", "content": "Palīdzi ar plānu"}, |
| ] |
|
|
|
|
| class _RecoveringChatClient: |
| def __init__(self, token: str | None = None) -> None: |
| self.token = token |
| self.chat_models: list[str] = [] |
|
|
| def chat_completion(self, **kwargs: object) -> dict[str, object]: |
| model = str(kwargs["model"]) |
| self.chat_models.append(model) |
| if model == "MarisUK/offline-model": |
| raise OSError("primary model unavailable") |
| return { |
| "choices": [ |
| { |
| "message": { |
| "content": f"Atbilde no {model}", |
| } |
| } |
| ] |
| } |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_generate_space_chat_reply_tries_fallback_models_when_primary_fails( |
| monkeypatch, |
| ) -> None: |
| async def fake_save_conversation( |
| self, |
| prompt: str, |
| response: str, |
| metadata: dict[str, object] | None = None, |
| ) -> None: |
| del self, prompt, response, metadata |
|
|
| monkeypatch.setattr(HFIntegration, "save_conversation", fake_save_conversation) |
| monkeypatch.setenv("MARIS_CHAT_MODEL", "MarisUK/offline-model") |
| monkeypatch.setenv("MARIS_CHAT_MODELS", "") |
| monkeypatch.setenv("MARIS_CHAT_FALLBACK_MODELS", "Qwen/Qwen3-Coder-480B-A35B-Instruct") |
|
|
| response = await generate_space_chat_reply( |
| SpaceChatRequest(message="Sveiki"), |
| client_factory=_RecoveringChatClient, |
| ) |
|
|
| assert response.response == "Atbilde no Qwen/Qwen3-Coder-480B-A35B-Instruct" |
| assert response.model == "Qwen/Qwen3-Coder-480B-A35B-Instruct" |
|
|
|
|
| class _UnavailableChatClient: |
| def __init__(self, token: str | None = None) -> None: |
| self.token = token |
|
|
| def chat_completion(self, **kwargs: object) -> dict[str, object]: |
| del kwargs |
| raise RuntimeError("provider rejected model") |
|
|
| def text_generation(self, **kwargs: object) -> str: |
| del kwargs |
| raise RuntimeError("generation unavailable") |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_generate_space_chat_reply_returns_emergency_fallback_when_all_models_fail( |
| monkeypatch, |
| ) -> None: |
| async def fake_save_conversation( |
| self, |
| prompt: str, |
| response: str, |
| metadata: dict[str, object] | None = None, |
| ) -> None: |
| del self, prompt, metadata |
| assert "drošo fallback režīmu" in response |
|
|
| monkeypatch.setattr(HFIntegration, "save_conversation", fake_save_conversation) |
|
|
| response = await generate_space_chat_reply( |
| SpaceChatRequest(message="Sveiki", model="deepseek-ai/DeepSeek-V3.2"), |
| client_factory=_UnavailableChatClient, |
| ) |
|
|
| assert "drošo fallback režīmu" in response.response |
| assert response.model == DEFAULT_CHAT_MODEL |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_generate_space_chat_reply_saves_rich_metadata(monkeypatch) -> None: |
| captured_metadata: dict[str, object] = {} |
|
|
| async def fake_save_conversation( |
| self, |
| prompt: str, |
| response: str, |
| metadata: dict[str, object] | None = None, |
| ) -> None: |
| del self |
| assert prompt == "Sveiki" |
| assert response == "Atbilde no MarisUK/maris-ai-text" |
| captured_metadata.update(metadata or {}) |
|
|
| class _HealthyChatClient: |
| def __init__(self, token: str | None = None) -> None: |
| self.token = token |
|
|
| def chat_completion(self, **kwargs: object) -> dict[str, object]: |
| return { |
| "choices": [ |
| { |
| "message": { |
| "content": f"Atbilde no {kwargs['model']}", |
| } |
| } |
| ] |
| } |
|
|
| monkeypatch.setattr(HFIntegration, "save_conversation", fake_save_conversation) |
|
|
| response = await generate_space_chat_reply( |
| SpaceChatRequest( |
| message="Sveiki", |
| history=[{"role": "user", "content": "Sveiki"}], |
| persona_id="strategist", |
| session_id="space-session-1", |
| ), |
| client_factory=_HealthyChatClient, |
| ) |
|
|
| assert response.model == DEFAULT_CHAT_MODEL |
| assert captured_metadata == { |
| "session_id": "space-session-1", |
| "persona_id": "strategist", |
| "requested_model": DEFAULT_CHAT_MODEL, |
| "resolved_model": DEFAULT_CHAT_MODEL, |
| "history_messages": 1, |
| "detected_emotion": "neutral", |
| "emotion_confidence": 0.45, |
| "response_style": "clear_grounded", |
| "space_repo": "MarisUK/maris.ai.chat", |
| "public_space_chat": True, |
| } |
|
|