mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-04 16:13:23 +00:00
The backend import package is now docsgpt, the name it will carry on PyPI; application was far too generic to install into anyone's site-packages. git mv plus a mechanical rewrite of every import, dotted string and path reference: 734 Python files, the compose files, Dockerfile, workflows, docs, setup scripts, devcontainer, k8s manifests, vscode config, pytest and coverage config, .gitignore. Behaviour is unchanged. Kept for one release: - A top-level application package whose meta-path finder resolves application.x.y to the already-imported docsgpt.x.y object, so old imports and entry points (celery -A application.app.celery, uvicorn application.asgi:asgi_app) keep working with a FutureWarning. - Celery registers every application.* task name as an alias of its docsgpt.* task on start-up, so messages queued by the previous release still run. The redbeat key prefix moves to redbeat:docsgpt:v2: so schedule entries the previous release wrote are left unread instead of firing twice. The backend image builds from the repository root (docker build -f docsgpt/Dockerfile .) so it can ship the alias package; a root .dockerignore allow-lists docsgpt/ and application/ and keeps caches, local data, .env files, the sample index files and the Dockerfile out. Compose and the image workflows point at the new context.
205 lines
6.4 KiB
Python
205 lines
6.4 KiB
Python
"""Unit tests for docsgpt/llm/base.py — BaseLLM.
|
|
|
|
Covers initialisation, static helpers, supports_* introspection,
|
|
structured-output defaults, and attachment-type defaults.
|
|
Fallback behaviour is covered separately in test_fallback.py.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, Mock
|
|
|
|
import pytest
|
|
|
|
from docsgpt.llm.base import BaseLLM
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Concrete stub so we can instantiate the abstract base
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class StubLLM(BaseLLM):
|
|
"""Minimal concrete BaseLLM for unit-testing non-abstract members."""
|
|
|
|
def _raw_gen(self, baseself, model, messages, stream, tools=None, **kw):
|
|
return "raw_gen_result"
|
|
|
|
def _raw_gen_stream(self, baseself, model, messages, stream, tools=None, **kw):
|
|
yield "chunk"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# __init__
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestBaseLLMInit:
|
|
|
|
def test_defaults(self):
|
|
llm = StubLLM()
|
|
assert llm.decoded_token is None
|
|
assert llm.agent_id is None
|
|
assert llm.model_id is None
|
|
assert llm.base_url is None
|
|
assert llm.token_usage == {"prompt_tokens": 0, "generated_tokens": 0}
|
|
assert llm._backup_models == []
|
|
assert llm._fallback_llm is None
|
|
|
|
def test_agent_id_cast_to_str(self):
|
|
llm = StubLLM(agent_id=42)
|
|
assert llm.agent_id == "42"
|
|
|
|
def test_agent_id_none_stays_none(self):
|
|
llm = StubLLM(agent_id=None)
|
|
assert llm.agent_id is None
|
|
|
|
def test_custom_params(self):
|
|
token = {"sub": "u1"}
|
|
llm = StubLLM(
|
|
decoded_token=token,
|
|
agent_id="abc",
|
|
model_id="gpt-4",
|
|
base_url="http://x",
|
|
backup_models=["m1", "m2"],
|
|
)
|
|
assert llm.decoded_token is token
|
|
assert llm.agent_id == "abc"
|
|
assert llm.model_id == "gpt-4"
|
|
assert llm.base_url == "http://x"
|
|
assert llm._backup_models == ["m1", "m2"]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _remove_null_values
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestRemoveNullValues:
|
|
|
|
def test_removes_none_values(self):
|
|
result = BaseLLM._remove_null_values({"a": 1, "b": None, "c": "x"})
|
|
assert result == {"a": 1, "c": "x"}
|
|
|
|
def test_keeps_falsy_non_none(self):
|
|
result = BaseLLM._remove_null_values({"a": 0, "b": "", "c": False, "d": []})
|
|
assert result == {"a": 0, "b": "", "c": False, "d": []}
|
|
|
|
def test_non_dict_passthrough(self):
|
|
assert BaseLLM._remove_null_values("hello") == "hello"
|
|
assert BaseLLM._remove_null_values(42) == 42
|
|
assert BaseLLM._remove_null_values([1, 2]) == [1, 2]
|
|
|
|
def test_empty_dict(self):
|
|
assert BaseLLM._remove_null_values({}) == {}
|
|
|
|
def test_all_none(self):
|
|
assert BaseLLM._remove_null_values({"a": None, "b": None}) == {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# supports_tools / _supports_tools
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestSupportsTools:
|
|
|
|
def test_supports_tools_true_when_callable(self):
|
|
llm = StubLLM()
|
|
assert llm.supports_tools() is True
|
|
|
|
def test_supports_tools_false_when_not_callable(self):
|
|
llm = StubLLM()
|
|
llm._supports_tools = "not_callable"
|
|
assert llm.supports_tools() is False
|
|
|
|
def test_default_supports_tools_raises(self):
|
|
llm = StubLLM()
|
|
with pytest.raises(NotImplementedError):
|
|
llm._supports_tools()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# supports_structured_output / _supports_structured_output
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestSupportsStructuredOutput:
|
|
|
|
def test_supports_structured_output_true(self):
|
|
llm = StubLLM()
|
|
assert llm.supports_structured_output() is True
|
|
|
|
def test_default_supports_structured_output_returns_false(self):
|
|
llm = StubLLM()
|
|
assert llm._supports_structured_output() is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# prepare_structured_output_format
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestPrepareStructuredOutputFormat:
|
|
|
|
def test_returns_none_by_default(self):
|
|
llm = StubLLM()
|
|
assert llm.prepare_structured_output_format({"type": "object"}) is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_supported_attachment_types
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestGetSupportedAttachmentTypes:
|
|
|
|
def test_returns_empty_list(self):
|
|
llm = StubLLM()
|
|
assert llm.get_supported_attachment_types() == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# fallback_llm — caching
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestFallbackLLMCaching:
|
|
|
|
def test_returns_cached_instance(self, monkeypatch):
|
|
"""Once resolved, the same fallback instance is returned."""
|
|
sentinel = StubLLM()
|
|
llm = StubLLM()
|
|
llm._fallback_llm = sentinel
|
|
assert llm.fallback_llm is sentinel
|
|
|
|
def test_none_when_no_backup_and_no_global(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"docsgpt.llm.base.settings",
|
|
MagicMock(FALLBACK_LLM_PROVIDER=None),
|
|
)
|
|
llm = StubLLM(backup_models=[])
|
|
assert llm.fallback_llm is None
|
|
|
|
def test_global_fallback_init_failure_returns_none(self, monkeypatch):
|
|
monkeypatch.setattr(
|
|
"docsgpt.llm.base.settings",
|
|
MagicMock(
|
|
FALLBACK_LLM_PROVIDER="openai",
|
|
FALLBACK_LLM_NAME="gpt-4",
|
|
FALLBACK_LLM_API_KEY="k",
|
|
API_KEY="k",
|
|
),
|
|
)
|
|
monkeypatch.setattr(
|
|
"docsgpt.llm.llm_creator.LLMCreator.create_llm",
|
|
Mock(side_effect=RuntimeError("boom")),
|
|
)
|
|
llm = StubLLM(backup_models=[])
|
|
assert llm.fallback_llm is None
|