feat(rag): lite-model document summaries — non-markdown docs summarized at import, summary chunk retrieves and resolves to the full source doc
This commit is contained in:
@@ -275,17 +275,42 @@ class _FakeChatStream:
|
||||
return chunk
|
||||
|
||||
|
||||
class _FakeCompletion:
|
||||
"""One fake non-streaming ChatCompletion (``choices[].message`` shape).
|
||||
|
||||
``content=None`` mirrors the real wire where the field can be absent or
|
||||
empty (reasoning-only replies, provider quirks).
|
||||
"""
|
||||
|
||||
def __init__(self, content: str | None, empty_choices: bool = False) -> None:
|
||||
if empty_choices:
|
||||
self.choices = []
|
||||
else:
|
||||
self.choices = [SimpleNamespace(message=SimpleNamespace(content=content))]
|
||||
|
||||
|
||||
class _FakeCompletions:
|
||||
def __init__(self, chunks: list | None = None, fail: Exception | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
chunks: list | None = None,
|
||||
fail: Exception | None = None,
|
||||
completion: _FakeCompletion | None = None,
|
||||
) -> None:
|
||||
self.chunks = chunks or []
|
||||
self.fail = fail
|
||||
self.completion = completion
|
||||
self.kwargs: dict | None = None
|
||||
self.chat_kwargs: dict | None = None
|
||||
|
||||
async def create(self, **kwargs) -> _FakeChatStream:
|
||||
async def create(self, **kwargs) -> _FakeChatStream | _FakeCompletion:
|
||||
self.kwargs = kwargs
|
||||
if self.fail is not None:
|
||||
raise self.fail
|
||||
return _FakeChatStream(self.chunks)
|
||||
if kwargs.get("stream"):
|
||||
return _FakeChatStream(self.chunks)
|
||||
self.chat_kwargs = kwargs
|
||||
assert self.completion is not None
|
||||
return self.completion
|
||||
|
||||
|
||||
def _make_stream_client(
|
||||
@@ -428,3 +453,85 @@ def test_chat_stream_llm_error_passes_through_unwrapped() -> None:
|
||||
llm, _ = _make_stream_client(fail=LLMError("already wrapped"))
|
||||
with pytest.raises(LLMError, match="already wrapped"):
|
||||
asyncio.run(_collect(llm, [{"role": "user", "content": "q"}]))
|
||||
|
||||
|
||||
# ---------- one-shot chat: LLMClient.chat (phase 30, task 01) ----------
|
||||
|
||||
|
||||
def _make_chat_client(
|
||||
completion: _FakeCompletion | None = None,
|
||||
fail: Exception | None = None,
|
||||
**settings_kwargs: Any,
|
||||
) -> tuple[LLMClient, _FakeCompletions]:
|
||||
completions = _FakeCompletions(fail=fail, completion=completion)
|
||||
fake_openai = SimpleNamespace(chat=SimpleNamespace(completions=completions))
|
||||
llm = LLMClient(_settings(**settings_kwargs))
|
||||
llm._client = fake_openai # pyright: ignore[reportAttributeAccessIssue]
|
||||
return llm, completions
|
||||
|
||||
|
||||
def test_chat_returns_trimmed_content_with_locked_params() -> None:
|
||||
"""Default model is ``lite`` (BOR_LLM_SUMMARY_MODEL), non-streaming,
|
||||
low temperature, fixed 2048-token budget — summaries are short."""
|
||||
llm, completions = _make_chat_client(_FakeCompletion(" Summary text.\n"))
|
||||
messages = [{"role": "system", "content": "s"}, {"role": "user", "content": "u"}]
|
||||
out = asyncio.run(llm.chat(messages))
|
||||
assert out == "Summary text."
|
||||
assert completions.chat_kwargs is not None
|
||||
assert completions.chat_kwargs["model"] == "lite"
|
||||
assert completions.chat_kwargs["stream"] is False
|
||||
assert completions.chat_kwargs["temperature"] == 0.2
|
||||
assert completions.chat_kwargs["max_tokens"] == 2048
|
||||
assert completions.chat_kwargs["messages"] == messages
|
||||
|
||||
|
||||
def test_chat_default_model_comes_from_llm_summary_model_setting() -> None:
|
||||
llm, completions = _make_chat_client(
|
||||
_FakeCompletion("x"), llm_summary_model="tiny"
|
||||
)
|
||||
asyncio.run(llm.chat([{"role": "user", "content": "q"}]))
|
||||
assert completions.chat_kwargs is not None
|
||||
assert completions.chat_kwargs["model"] == "tiny"
|
||||
|
||||
|
||||
def test_chat_explicit_model_overrides_the_default() -> None:
|
||||
llm, completions = _make_chat_client(
|
||||
_FakeCompletion("x"), llm_summary_model="tiny"
|
||||
)
|
||||
asyncio.run(llm.chat([{"role": "user", "content": "q"}], model="special"))
|
||||
assert completions.chat_kwargs is not None
|
||||
assert completions.chat_kwargs["model"] == "special"
|
||||
|
||||
|
||||
def test_chat_transport_failure_wrapped_as_llm_error_with_base_url() -> None:
|
||||
"""HTTP/transport failures (incl. >=400 surfaced by the SDK) are wrapped
|
||||
with the base URL in the message — same style as chat_stream."""
|
||||
llm, _ = _make_chat_client(fail=RuntimeError("HTTP 502 Bad Gateway"))
|
||||
with pytest.raises(LLMError, match="HTTP 502") as exc:
|
||||
asyncio.run(llm.chat([{"role": "user", "content": "q"}]))
|
||||
assert "aipi.reeseapps.com" in str(exc.value)
|
||||
|
||||
|
||||
def test_chat_llm_error_passes_through_unwrapped() -> None:
|
||||
llm, _ = _make_chat_client(fail=LLMError("already wrapped"))
|
||||
with pytest.raises(LLMError, match="already wrapped"):
|
||||
asyncio.run(llm.chat([{"role": "user", "content": "q"}]))
|
||||
|
||||
|
||||
def test_chat_empty_choices_raises_llm_error() -> None:
|
||||
llm, _ = _make_chat_client(_FakeCompletion(None, empty_choices=True))
|
||||
with pytest.raises(LLMError, match="no choices"):
|
||||
asyncio.run(llm.chat([{"role": "user", "content": "q"}]))
|
||||
|
||||
|
||||
def test_chat_missing_content_raises_llm_error() -> None:
|
||||
"""A silent empty summary must never be stored — None content fails."""
|
||||
llm, _ = _make_chat_client(_FakeCompletion(None))
|
||||
with pytest.raises(LLMError, match="empty content"):
|
||||
asyncio.run(llm.chat([{"role": "user", "content": "q"}]))
|
||||
|
||||
|
||||
def test_chat_whitespace_only_content_raises_llm_error() -> None:
|
||||
llm, _ = _make_chat_client(_FakeCompletion(" \n\t "))
|
||||
with pytest.raises(LLMError, match="empty content"):
|
||||
asyncio.run(llm.chat([{"role": "user", "content": "q"}]))
|
||||
|
||||
Reference in New Issue
Block a user