File size: 10,294 Bytes
f440f03
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
"""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,
    }