Refactor settings store to rename proModel to proThinking and update related logic; enhance CSS for energy efficiency and reduced motion preferences; improve i18n translations for better clarity and consistency; modify proBlock utility functions for clearer instruction handling; streamline Vite configuration by removing unnecessary Univer.js dependencies.
This commit is contained in:
+148
-178
@@ -2,6 +2,7 @@ import asyncio
|
||||
import importlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -15,66 +16,27 @@ except ModuleNotFoundError:
|
||||
pytest.skip("llm module dependencies are not available", allow_module_level=True)
|
||||
|
||||
|
||||
def test_extract_message_with_object_message_content_and_thinking():
|
||||
class Msg:
|
||||
def __init__(self, content, thinking):
|
||||
self.content = content
|
||||
self.thinking = thinking
|
||||
|
||||
class Resp:
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
resp = Resp(Msg("hello world", "thinking about it"))
|
||||
def test_extract_message_with_content_and_thinking():
|
||||
resp = {"choices": [{"message": {"content": "hello world", "thinking": "reasoning"}}]}
|
||||
content, thinking = llm._extract_message(resp)
|
||||
assert content == "hello world"
|
||||
assert thinking == "thinking about it"
|
||||
assert thinking == "reasoning"
|
||||
|
||||
|
||||
def test_extract_message_with_object_message_empty_content():
|
||||
class Msg:
|
||||
def __init__(self, content, thinking):
|
||||
self.content = content
|
||||
self.thinking = thinking
|
||||
|
||||
class Resp:
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
resp = Resp(Msg("", None))
|
||||
def test_extract_message_empty_content():
|
||||
resp = {"choices": [{"message": {"content": "", "thinking": None}}]}
|
||||
content, thinking = llm._extract_message(resp)
|
||||
assert content == ""
|
||||
assert thinking == ""
|
||||
|
||||
|
||||
def test_extract_message_with_dict_message():
|
||||
resp = {"message": {"content": "ok", "thinking": "calc"}}
|
||||
content, thinking = llm._extract_message(resp)
|
||||
assert content == "ok"
|
||||
assert thinking == "calc"
|
||||
|
||||
|
||||
def test_extract_message_dict_no_message_key():
|
||||
resp = {"not_message": {"content": "irrelevant"}}
|
||||
def test_extract_message_dict_no_choices():
|
||||
resp = {"not_choices": []}
|
||||
content, thinking = llm._extract_message(resp)
|
||||
assert content == ""
|
||||
assert thinking == ""
|
||||
|
||||
|
||||
def test_extract_message_dict_message_content_none_and_thinking_none():
|
||||
resp = {"message": {"content": None, "thinking": None}}
|
||||
content, thinking = llm._extract_message(resp)
|
||||
assert content == ""
|
||||
assert thinking == ""
|
||||
|
||||
|
||||
def test_extract_message_dict_message_thinking_none():
|
||||
resp = {"message": {"content": "val", "thinking": None}}
|
||||
content, thinking = llm._extract_message(resp)
|
||||
assert content == "val"
|
||||
assert thinking == ""
|
||||
|
||||
|
||||
def test_extract_message_empty_dict():
|
||||
resp = {}
|
||||
content, thinking = llm._extract_message(resp)
|
||||
@@ -82,174 +44,182 @@ def test_extract_message_empty_dict():
|
||||
assert thinking == ""
|
||||
|
||||
|
||||
def test_call_ollama_no_system_message(monkeypatch):
|
||||
def test_extract_delta_text_from_chunk():
|
||||
chunk = {"choices": [{"delta": {"content": "text"}}]}
|
||||
assert llm._extract_delta_text(chunk) == "text"
|
||||
|
||||
|
||||
def test_extract_delta_thinking_from_chunk():
|
||||
chunk = {"choices": [{"delta": {"thinking": "thought"}}]}
|
||||
assert llm._extract_delta_thinking(chunk) == "thought"
|
||||
|
||||
|
||||
def test_call_ollama_no_system(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
async def fake_generate(**kwargs):
|
||||
captured["kwargs"] = kwargs
|
||||
return {"response": "ok"}
|
||||
async def fake_post(url, json=None):
|
||||
captured["json"] = json
|
||||
|
||||
monkeypatch.setattr(llm.client, "generate", fake_generate)
|
||||
class FakeResp:
|
||||
def raise_for_status(self): pass
|
||||
def json(self): return {"choices": [{"message": {"content": "ok"}}]}
|
||||
|
||||
return FakeResp()
|
||||
|
||||
async def fake_client(*args, **kwargs):
|
||||
class Ctx:
|
||||
async def __aenter__(self2): return self2
|
||||
async def __aexit__(*a): pass
|
||||
post = fake_post
|
||||
return Ctx()
|
||||
|
||||
monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
|
||||
monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)
|
||||
|
||||
result = asyncio.run(
|
||||
llm.call_ollama("user prompt body", system_prompt=None, tag="no-system", temperature=0.1)
|
||||
llm.call_ollama("user prompt", system_prompt=None, tag="no-system")
|
||||
)
|
||||
|
||||
assert result["content"] == "ok"
|
||||
assert captured["kwargs"]["prompt"] == "user prompt body"
|
||||
assert captured["kwargs"]["raw"] is True
|
||||
# Should only have user message, no system
|
||||
assert len(captured["json"]["messages"]) == 1
|
||||
|
||||
|
||||
def test_call_ollama_whitespace_system_message(monkeypatch):
|
||||
def test_call_ollama_with_system(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
async def fake_generate(**kwargs):
|
||||
captured["kwargs"] = kwargs
|
||||
return {"response": "ok"}
|
||||
async def fake_post(url, json=None):
|
||||
captured["json"] = json
|
||||
|
||||
monkeypatch.setattr(llm.client, "generate", fake_generate)
|
||||
class FakeResp:
|
||||
def raise_for_status(self): pass
|
||||
def json(self): return {"choices": [{"message": {"content": "ok"}}]}
|
||||
|
||||
return FakeResp()
|
||||
|
||||
async def fake_client(*args, **kwargs):
|
||||
class Ctx:
|
||||
async def __aenter__(self2): return self2
|
||||
async def __aexit__(*a): pass
|
||||
post = fake_post
|
||||
return Ctx()
|
||||
|
||||
monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
|
||||
monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)
|
||||
|
||||
result = asyncio.run(
|
||||
llm.call_ollama("user prompt", system_prompt=" ", tag="whitespace-system", temperature=0.1)
|
||||
llm.call_ollama("user prompt", system_prompt="sys prompt", tag="with-system")
|
||||
)
|
||||
|
||||
assert result["content"] == "ok"
|
||||
assert captured["kwargs"]["prompt"] == "user prompt"
|
||||
assert captured["kwargs"]["raw"] is True
|
||||
# Should have both system and user messages
|
||||
msgs = captured["json"]["messages"]
|
||||
assert len(msgs) == 2
|
||||
assert msgs[0]["role"] == "system"
|
||||
|
||||
|
||||
def test_call_ollama_thinking_in_kwargs(monkeypatch):
|
||||
def test_call_ollama_with_custom_model(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
async def fake_generate(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"response": "ok", "message": {"thinking": "boom"}} # keep thinking for backward test though it might not be perfect
|
||||
async def fake_post(url, json=None):
|
||||
captured["json"] = json
|
||||
|
||||
monkeypatch.setattr(llm.client, "generate", fake_generate)
|
||||
class FakeResp:
|
||||
def raise_for_status(self): pass
|
||||
def json(self): return {"choices": [{"message": {"content": "ok"}}]}
|
||||
|
||||
res = asyncio.run(
|
||||
llm.call_ollama("prompt", thinking="boom", tag="think-flag", temperature=0.7)
|
||||
return FakeResp()
|
||||
|
||||
async def fake_client(*args, **kwargs):
|
||||
class Ctx:
|
||||
async def __aenter__(self2): return self2
|
||||
async def __aexit__(*a): pass
|
||||
post = fake_post
|
||||
return Ctx()
|
||||
|
||||
monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
|
||||
monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)
|
||||
|
||||
result = asyncio.run(
|
||||
llm.call_ollama("prompt", model="custom-model")
|
||||
)
|
||||
assert res["content"] == "ok" and res["think"] == "boom"
|
||||
assert captured.get("think") == "boom"
|
||||
|
||||
assert captured["json"]["model"] == "custom-model"
|
||||
|
||||
|
||||
def test_call_ollama_cancelled_reraises(monkeypatch):
|
||||
async def fake_generate(**kwargs):
|
||||
raise asyncio.CancelledError
|
||||
|
||||
monkeypatch.setattr(llm.client, "generate", fake_generate)
|
||||
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
asyncio.run(
|
||||
llm.call_ollama("prompt", system_prompt=None, tag="cancel", temperature=0.7)
|
||||
)
|
||||
|
||||
|
||||
def test_call_ollama_chat_raises_rethrows(monkeypatch):
|
||||
async def fake_generate(**kwargs):
|
||||
raise ValueError("boom")
|
||||
|
||||
monkeypatch.setattr(llm.client, "generate", fake_generate)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
asyncio.run(
|
||||
llm.call_ollama("prompt", system_prompt=None, tag="exception", temperature=0.7)
|
||||
)
|
||||
|
||||
|
||||
def test_call_ollama_returns_content_and_think_from_response(monkeypatch):
|
||||
async def fake_generate(**kwargs):
|
||||
return {"response": "final", "message": {"thinking": "process"}}
|
||||
|
||||
monkeypatch.setattr(llm.client, "generate", fake_generate)
|
||||
|
||||
res = asyncio.run(
|
||||
llm.call_ollama("prompt", system_prompt=None, tag="return", temperature=0.7)
|
||||
)
|
||||
assert res["content"] == "final" and res["think"] == "process"
|
||||
|
||||
|
||||
def test_stream_ollama_uses_requested_model_and_yields_chunks(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
class FakeStream:
|
||||
def __init__(self, chunks):
|
||||
self._chunks = iter(chunks)
|
||||
|
||||
def __aiter__(self):
|
||||
return self
|
||||
|
||||
async def __anext__(self):
|
||||
try:
|
||||
return next(self._chunks)
|
||||
except StopIteration:
|
||||
raise StopAsyncIteration
|
||||
|
||||
async def fake_generate(**kwargs):
|
||||
captured["kwargs"] = kwargs
|
||||
return FakeStream([
|
||||
{"response": "深度"},
|
||||
{"response": "回答"},
|
||||
def test_stream_ollama_events_error_handling(monkeypatch):
|
||||
def make_lines():
|
||||
lines_iter = iter([
|
||||
'data: {"error": "model not found"}',
|
||||
])
|
||||
|
||||
monkeypatch.setattr(llm.client, "generate", fake_generate)
|
||||
class LineIterator:
|
||||
async def __anext__(self):
|
||||
try:
|
||||
return next(lines_iter)
|
||||
except StopIteration:
|
||||
raise StopAsyncIteration()
|
||||
|
||||
chunks = []
|
||||
class Response:
|
||||
def __init__(self2): self2._lines = LineIterator()
|
||||
|
||||
async def collect_stream():
|
||||
async for chunk in llm.stream_ollama(
|
||||
"prompt",
|
||||
system_prompt="system",
|
||||
tag="stream",
|
||||
temperature=0.8,
|
||||
model="pro-model",
|
||||
use_pro_model=True,
|
||||
):
|
||||
chunks.append(chunk)
|
||||
async def raise_for_status(self2): pass
|
||||
async def aiter_lines(self2): return self2._lines
|
||||
|
||||
asyncio.run(collect_stream())
|
||||
class StreamCtx:
|
||||
async def __aenter__(self2): return Response()
|
||||
async def __aexit__(*a): pass
|
||||
|
||||
assert "".join(chunks) == "深度回答"
|
||||
assert captured["kwargs"]["model"] == "pro-model"
|
||||
assert captured["kwargs"]["stream"] is True
|
||||
class Client:
|
||||
stream = lambda self2, *args, **kw: StreamCtx()
|
||||
|
||||
return Client()
|
||||
|
||||
async def fake_client(*args, **kwargs):
|
||||
return make_lines()
|
||||
|
||||
monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
|
||||
monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)
|
||||
|
||||
async def collect():
|
||||
try:
|
||||
async for _ in llm.stream_ollama_events("prompt", tag="err"):
|
||||
pass
|
||||
except RuntimeError as e:
|
||||
return str(e)
|
||||
|
||||
result = asyncio.run(collect())
|
||||
assert "model not found" in str(result)
|
||||
|
||||
|
||||
def test_call_vlm_ocr_passes_image_and_prompt(monkeypatch):
|
||||
image_bytes = b"image-bytes"
|
||||
called = {}
|
||||
monkeypatch.setattr(llm, "get_vlm_ocr_prompt", lambda: "OCR PROMPT")
|
||||
def test_call_vlm_ocr_payload_format(monkeypatch):
|
||||
captured = {}
|
||||
|
||||
async def fake_chat(**kwargs):
|
||||
called["kwargs"] = kwargs
|
||||
return {"message": {"content": "ocr result", "thinking": ""}}
|
||||
async def fake_post(url, json=None):
|
||||
captured["json"] = json
|
||||
|
||||
monkeypatch.setattr(llm.client, "chat", fake_chat)
|
||||
class FakeResp:
|
||||
def raise_for_status(self): pass
|
||||
def json(self): return {"choices": [{"message": {"content": "ocr result"}}]}
|
||||
|
||||
result = asyncio.run(llm.call_vlm_ocr(image_bytes, language="auto"))
|
||||
return FakeResp()
|
||||
|
||||
messages = called["kwargs"].get("messages", [])
|
||||
assert messages[0]["role"] == "user"
|
||||
assert messages[0]["content"] == "OCR PROMPT"
|
||||
assert messages[0]["images"] == [image_bytes]
|
||||
async def fake_client(*args, **kwargs):
|
||||
class Ctx:
|
||||
async def __aenter__(self2): return self2
|
||||
async def __aexit__(*a): pass
|
||||
post = fake_post
|
||||
return Ctx()
|
||||
|
||||
monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
|
||||
monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)
|
||||
|
||||
result = asyncio.run(llm.call_vlm_ocr(b"image"))
|
||||
assert result == "ocr result"
|
||||
|
||||
|
||||
def test_call_vlm_ocr_chat_raises_rethrows(monkeypatch):
|
||||
image_bytes = b"image-bytes"
|
||||
monkeypatch.setattr(llm, "get_vlm_ocr_prompt", lambda: "OCR PROMPT")
|
||||
|
||||
async def fake_chat(**kwargs):
|
||||
raise RuntimeError("ocr fail")
|
||||
monkeypatch.setattr(llm.client, "chat", fake_chat)
|
||||
with pytest.raises(RuntimeError):
|
||||
asyncio.run(llm.call_vlm_ocr(image_bytes))
|
||||
|
||||
|
||||
def test_call_vlm_ocr_returns_content_from_response(monkeypatch):
|
||||
image_bytes = b"img"
|
||||
monkeypatch.setattr(llm, "get_vlm_ocr_prompt", lambda: "OCR PROMPT")
|
||||
|
||||
async def fake_chat(**kwargs):
|
||||
return {"message": {"content": "ocr text", "thinking": ""}}
|
||||
monkeypatch.setattr(llm.client, "chat", fake_chat)
|
||||
content = asyncio.run(llm.call_vlm_ocr(image_bytes))
|
||||
assert content == "ocr text"
|
||||
# Verify vision format: image_url content part with base64
|
||||
msgs = captured["json"]["messages"]
|
||||
assert len(msgs) == 1
|
||||
content_parts = msgs[0]["content"]
|
||||
image_part = [p for p in content_parts if p.get("type") == "image_url"]
|
||||
assert len(image_part) == 1
|
||||
|
||||
Reference in New Issue
Block a user