refactor: rename the application package to docsgpt

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.
This commit is contained in:
Alex committed 2026-09-07 10:20:43 +01:00
1 parent 3e7f1f91b4
commit 574f96341e
984 files changed
+9847 -9682

No files matched your search

@@ -14,16 +14,16 @@ from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression import CompressionService
from application.api.answer.services.compression.orchestrator import (
from docsgpt.api.answer.services.compression import CompressionService
from docsgpt.api.answer.services.compression.orchestrator import (
CompressionOrchestrator,
)
from application.api.answer.services.compression.threshold_checker import (
from docsgpt.api.answer.services.compression.threshold_checker import (
CompressionThresholdChecker,
)
from application.api.answer.services.compression.token_counter import TokenCounter
from application.api.answer.services.compression.types import CompressionResult
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.compression.token_counter import TokenCounter
from docsgpt.api.answer.services.compression.types import CompressionResult
from docsgpt.api.answer.services.conversation_service import (
COMPRESSION_SUMMARY_PROMPT,
)
@@ -106,16 +106,16 @@ class TestTurnStartReuse:
assert result.last_compression_at is None
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_over_threshold_compresses_only_the_tail(
self,
mock_settings,
@@ -172,7 +172,7 @@ class TestEffectiveTokenCount:
)
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=1000,
)
def test_should_compress_uses_effective_count(self, _limit):
@@ -252,7 +252,7 @@ class TestSummaryRowMarker:
who types the label text as a question keeps that turn."""
def test_marked_row_is_a_summary_row(self):
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
COMPRESSION_SUMMARY_MARKER,
is_compression_summary_row,
)
@@ -261,7 +261,7 @@ class TestSummaryRowMarker:
assert is_compression_summary_row(row) is True
def test_legacy_row_without_metadata_is_a_summary_row(self):
from application.api.answer.services.compression.types import is_compression_summary_row
from docsgpt.api.answer.services.compression.types import is_compression_summary_row
assert is_compression_summary_row({"prompt": COMPRESSION_SUMMARY_PROMPT, "response": "S"}) is True
assert is_compression_summary_row(
@@ -269,7 +269,7 @@ class TestSummaryRowMarker:
) is True
def test_user_turn_with_the_label_text_is_kept(self):
from application.api.answer.services.compression.types import is_compression_summary_row
from docsgpt.api.answer.services.compression.types import is_compression_summary_row
real_turn = {
"prompt": COMPRESSION_SUMMARY_PROMPT,
@@ -309,16 +309,16 @@ class TestIncrementalTailExcludesSummaryRows:
svc.compress_conversation(conv, compress_up_to_index=2, start_index=2)
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_orchestrator_reuses_summary_when_only_summary_rows_follow_the_point(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service, threshold_checker,
@@ -355,16 +355,16 @@ class TestAbsolutePersistIndex:
assert metadata.query_index == 19
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_mid_execution_builds_on_the_carried_summary_and_persists_the_absolute_index(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service,
@@ -461,16 +461,16 @@ class TestUnusableSavedPoints:
assert [q["prompt"] for q in recent] == ["q2", "q3", "q4"]
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
return_value="openai",
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider",
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider",
return_value="sk",
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_recompression_starts_after_the_latest_usable_point(
self, mock_settings, MockCompressionService, MockLLMCreator, _key, _provider,
orchestrator, conversation_service, threshold_checker,
@@ -1,9 +1,9 @@
"""Tests for application/api/answer/services/compression/message_builder.py"""
"""Tests for docsgpt/api/answer/services/compression/message_builder.py"""
import pytest
from application.api.answer.services.compression.message_builder import MessageBuilder
from docsgpt.api.answer.services.compression.message_builder import MessageBuilder
@pytest.mark.unit
@@ -1,13 +1,13 @@
"""Tests for application/api/answer/services/compression/orchestrator.py"""
"""Tests for docsgpt/api/answer/services/compression/orchestrator.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression.orchestrator import (
from docsgpt.api.answer.services.compression.orchestrator import (
CompressionOrchestrator,
)
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
CompressionMetadata,
CompressionResult,
)
@@ -172,14 +172,14 @@ class TestCompressIfNeeded:
@pytest.mark.unit
class TestPerformCompression:
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_successful_compression(
self,
mock_settings,
@@ -228,13 +228,13 @@ class TestPerformCompression:
mock_svc_instance.compress_and_save.assert_called_once()
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_uses_compression_model_override(
self,
mock_settings,
@@ -253,7 +253,7 @@ class TestPerformCompression:
conversation = {"queries": [{"prompt": "q", "response": "r"}], "agent_id": "a"}
with patch(
"application.api.answer.services.compression.orchestrator.CompressionService"
"docsgpt.api.answer.services.compression.orchestrator.CompressionService"
) as MockCS:
mock_svc = MagicMock()
mock_svc.compress_and_save.return_value = MagicMock(
@@ -280,14 +280,14 @@ class TestPerformCompression:
)
@patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id"
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id"
)
@patch(
"application.api.answer.services.compression.orchestrator.get_api_key_for_provider"
"docsgpt.api.answer.services.compression.orchestrator.get_api_key_for_provider"
)
@patch("application.api.answer.services.compression.orchestrator.LLMCreator")
@patch("application.api.answer.services.compression.orchestrator.CompressionService")
@patch("application.api.answer.services.compression.orchestrator.settings")
@patch("docsgpt.api.answer.services.compression.orchestrator.LLMCreator")
@patch("docsgpt.api.answer.services.compression.orchestrator.CompressionService")
@patch("docsgpt.api.answer.services.compression.orchestrator.settings")
def test_no_queries_returns_no_compression(
self,
mock_settings,
@@ -327,9 +327,9 @@ class TestPerformCompression:
}
with patch(
"application.api.answer.services.compression.orchestrator.settings"
"docsgpt.api.answer.services.compression.orchestrator.settings"
) as mock_settings, patch(
"application.api.answer.services.compression.orchestrator.get_provider_from_model_id",
"docsgpt.api.answer.services.compression.orchestrator.get_provider_from_model_id",
side_effect=RuntimeError("provider error"),
):
mock_settings.COMPRESSION_MODEL_OVERRIDE = None
@@ -1,11 +1,11 @@
"""Tests for application/api/answer/services/compression/service.py"""
"""Tests for docsgpt/api/answer/services/compression/service.py"""
from unittest.mock import MagicMock, patch
import pytest
from application.api.answer.services.compression.service import CompressionService
from application.api.answer.services.compression.types import CompressionMetadata
from docsgpt.api.answer.services.compression.service import CompressionService
from docsgpt.api.answer.services.compression.types import CompressionMetadata
@pytest.fixture
@@ -48,11 +48,11 @@ def sample_conversation():
@pytest.mark.unit
class TestCompressionServiceInit:
@patch("application.api.answer.services.compression.service.settings")
@patch("docsgpt.api.answer.services.compression.service.settings")
def test_default_prompt_builder(self, mock_settings, mock_llm):
mock_settings.COMPRESSION_PROMPT_VERSION = "v1.0"
with patch(
"application.api.answer.services.compression.service.CompressionPromptBuilder"
"docsgpt.api.answer.services.compression.service.CompressionPromptBuilder"
):
svc = CompressionService(llm=mock_llm, model_id="gpt-4")
assert svc.llm is mock_llm
@@ -81,7 +81,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 1000
MockTC.count_message_tokens.return_value = 100
@@ -144,7 +144,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 500
MockTC.count_message_tokens.return_value = 50
@@ -170,7 +170,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 1000
MockTC.count_message_tokens.return_value = 0
@@ -195,7 +195,7 @@ class TestCompressConversation:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 100
with pytest.raises(RuntimeError, match="LLM error"):
@@ -222,7 +222,7 @@ class TestCompressAndSave:
)
with patch(
"application.api.answer.services.compression.service.TokenCounter"
"docsgpt.api.answer.services.compression.service.TokenCounter"
) as MockTC:
MockTC.count_query_tokens.return_value = 500
MockTC.count_message_tokens.return_value = 50
@@ -487,7 +487,7 @@ class TestBoundRecentQueries:
self, mock_llm, monkeypatch
):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
50,
raising=False,
)
@@ -507,7 +507,7 @@ class TestBoundRecentQueries:
def test_oversized_tool_result_is_trimmed(self, mock_llm, monkeypatch):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
50,
raising=False,
)
@@ -536,7 +536,7 @@ class TestBoundRecentQueries:
def test_zero_cap_disables_bounding(self, mock_llm, monkeypatch):
monkeypatch.setattr(
"application.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
"docsgpt.api.answer.services.compression.service.settings.COMPRESSION_RECENT_FIELD_MAX_TOKENS",
0,
raising=False,
)
@@ -7,18 +7,18 @@ import pytest
class TestCompressionThresholdChecker:
def _make_checker(self, pct=0.7):
from application.api.answer.services.compression.threshold_checker import (
from docsgpt.api.answer.services.compression.threshold_checker import (
CompressionThresholdChecker,
)
return CompressionThresholdChecker(threshold_percentage=pct)
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=8000,
)
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
return_value=6000,
)
def test_check_message_tokens_above_threshold(self, mock_count, mock_limit):
@@ -26,11 +26,11 @@ class TestCompressionThresholdChecker:
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is True
@patch(
"application.api.answer.services.compression.threshold_checker.get_token_limit",
"docsgpt.api.answer.services.compression.threshold_checker.get_token_limit",
return_value=8000,
)
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
return_value=1000,
)
def test_check_message_tokens_below_threshold(self, mock_count, mock_limit):
@@ -38,7 +38,7 @@ class TestCompressionThresholdChecker:
assert checker.check_message_tokens([{"role": "user"}], "gpt-4") is False
@patch(
"application.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
"docsgpt.api.answer.services.compression.threshold_checker.TokenCounter.count_message_tokens",
side_effect=Exception("Token error"),
)
def test_check_message_tokens_exception_returns_false(self, mock_count):
@@ -1,10 +1,10 @@
"""Tests for application/api/answer/services/compression/types.py"""
"""Tests for docsgpt/api/answer/services/compression/types.py"""
from datetime import datetime, timezone
import pytest
from application.api.answer.services.compression.types import (
from docsgpt.api.answer.services.compression.types import (
CompressionMetadata,
CompressionResult,
)
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/continuation_service.py using pg_conn."""
"""Tests for docsgpt/api/answer/services/continuation_service.py using pg_conn."""
from contextlib import contextmanager
from unittest.mock import patch
@@ -15,10 +15,10 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.continuation_service.db_readonly",
"docsgpt.api.answer.services.continuation_service.db_readonly",
_yield,
), patch(
"application.api.answer.services.continuation_service.db_session",
"docsgpt.api.answer.services.continuation_service.db_session",
_yield,
):
yield
@@ -26,21 +26,21 @@ def _patch_db(conn):
class TestMakeSerializable:
def test_uuid_becomes_string(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
u = uuid4()
assert _make_serializable(u) == str(u)
def test_dict_keys_stringified(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
got = _make_serializable({42: "a", "b": 2})
assert got == {"42": "a", "b": 2}
def test_list_elements_recursively_serialized(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
u = uuid4()
@@ -52,7 +52,7 @@ class TestMakeSerializable:
# the shared serialization module — base64 is lossless and round-
# trippable (UTF-8-replace silently corrupted binary payloads).
import base64
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
got = _make_serializable(b"hello")
@@ -60,7 +60,7 @@ class TestMakeSerializable:
def test_bytes_arbitrary_binary_roundtrips(self):
import base64
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
raw = b"\xff\xfe\x00\x10"
@@ -69,7 +69,7 @@ class TestMakeSerializable:
assert base64.b64decode(got) == raw
def test_passes_through_primitives(self):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
assert _make_serializable("hello") == "hello"
@@ -83,7 +83,7 @@ class TestMakeSerializable:
# rows, which would otherwise blow up json.dumps in pending_tool_state.
import json
from datetime import datetime, timezone
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
@@ -97,7 +97,7 @@ class TestMakeSerializable:
# where each tool row has timestamp fields buried under string keys.
import json
from datetime import datetime, timezone
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
@@ -116,7 +116,7 @@ class TestMakeSerializable:
def test_date_becomes_iso_string(self):
from datetime import date
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
_make_serializable,
)
assert _make_serializable(date(2026, 5, 2)) == "2026-05-02"
@@ -124,10 +124,10 @@ class TestMakeSerializable:
class TestContinuationServiceSaveLoad:
def test_save_and_load_state(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -152,7 +152,7 @@ class TestContinuationServiceSaveLoad:
assert loaded["messages"] == [{"role": "user", "content": "hi"}]
def test_load_state_returns_none_when_not_found(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -164,11 +164,11 @@ class TestContinuationServiceSaveLoad:
assert got is None
def test_claim_state_is_atomic_and_duplicate_is_conflict(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
ResumeInProgressError,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -193,10 +193,10 @@ class TestContinuationServiceSaveLoad:
service.claim_state(conv_id, user)
def test_expired_state_is_neither_loaded_nor_claimed(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -226,10 +226,10 @@ class TestContinuationServiceSaveLoad:
assert service.claim_state(conv_id, user) is None
def test_save_state_no_client_tools(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -253,10 +253,10 @@ class TestContinuationServiceSaveLoad:
assert loaded is not None
def test_delete_state_returns_true_when_deleted(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -279,7 +279,7 @@ class TestContinuationServiceSaveLoad:
assert got is True
def test_delete_state_returns_false_when_missing(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -301,7 +301,7 @@ class TestContinuationServiceLegacyIdHandling:
LEGACY_OBJECTID = "507f1f77bcf86cd799439011"
def test_load_state_unresolvable_legacy_id_returns_none(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -311,7 +311,7 @@ class TestContinuationServiceLegacyIdHandling:
assert got is None
def test_delete_state_unresolvable_legacy_id_returns_false(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -323,7 +323,7 @@ class TestContinuationServiceLegacyIdHandling:
def test_save_state_unresolvable_legacy_id_raises(self, pg_conn):
import pytest
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
@@ -341,10 +341,10 @@ class TestContinuationServiceLegacyIdHandling:
)
def test_load_state_resolves_backfilled_legacy_id(self, pg_conn):
from application.api.answer.services.continuation_service import (
from docsgpt.api.answer.services.continuation_service import (
ContinuationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/conversation_service.py."""
"""Tests for docsgpt/api/answer/services/conversation_service.py."""
from contextlib import contextmanager
from datetime import datetime, timezone
@@ -14,10 +14,10 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_yield,
), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_yield,
):
yield
@@ -25,13 +25,13 @@ def _patch_db(conn):
class TestConversationServiceGet:
def test_returns_none_when_no_conversation_id(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
assert ConversationService().get_conversation("", "u") is None
def test_returns_none_when_no_user_id(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
assert (
@@ -42,7 +42,7 @@ class TestConversationServiceGet:
)
def test_returns_none_when_not_found(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn):
@@ -52,10 +52,10 @@ class TestConversationServiceGet:
assert got is None
def test_returns_conversation_with_messages(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -74,7 +74,7 @@ class TestConversationServiceGet:
assert len(got["queries"]) == 1
def test_handles_exception_returns_none(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -84,7 +84,7 @@ class TestConversationServiceGet:
yield
with patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_broken,
):
got = ConversationService().get_conversation("abc", "u")
@@ -93,7 +93,7 @@ class TestConversationServiceGet:
class TestConversationServiceSave:
def test_raises_for_none_token(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -105,7 +105,7 @@ class TestConversationServiceSave:
)
def test_raises_when_no_user_in_token(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -117,10 +117,10 @@ class TestConversationServiceSave:
)
def test_creates_new_conversation(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -141,10 +141,10 @@ class TestConversationServiceSave:
assert got["name"] == "Title"
def test_appends_message_to_existing(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -166,10 +166,10 @@ class TestConversationServiceSave:
assert any(m["prompt"] == "q-new" for m in messages)
def test_updates_message_at_index(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -193,7 +193,7 @@ class TestConversationServiceSave:
assert messages[0]["prompt"] == "updated"
def test_raises_when_conversation_missing(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn), pytest.raises(ValueError):
@@ -206,10 +206,10 @@ class TestConversationServiceSave:
)
def test_save_with_empty_llm_title_falls_back(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -231,11 +231,11 @@ class TestConversationServiceSave:
class TestSaveUserQuestion:
def test_creates_conversation_and_reserves_message(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
TERMINATED_RESPONSE_PLACEHOLDER,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -261,10 +261,10 @@ class TestSaveUserQuestion:
assert messages[0]["request_id"] == result["request_id"]
def test_appends_to_existing_conversation(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -285,7 +285,7 @@ class TestSaveUserQuestion:
assert msgs[0]["prompt"] == "follow-up"
def test_raises_when_token_missing(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with pytest.raises(ValueError):
@@ -302,10 +302,10 @@ class TestSaveUserQuestion:
the end. Pre-fix the WAL path appended unconditionally and the
old answer survived alongside the regenerated one.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -356,10 +356,10 @@ class TestSaveUserQuestion:
"""``index=0`` is a valid edge: it should drop every prior
message and reseat the placeholder at position 0.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -398,10 +398,10 @@ class TestSaveUserQuestion:
the create-then-reserve path silently treats it as a no-op
rather than truncating a freshly-created conversation.
"""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -421,7 +421,7 @@ class TestSaveUserQuestion:
assert msgs[0]["prompt"] == "brand new q"
def test_raises_when_conversation_unauthorized(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn), pytest.raises(ValueError):
@@ -434,10 +434,10 @@ class TestSaveUserQuestion:
class TestFinalizeMessage:
def test_finalizes_complete(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -449,7 +449,7 @@ class TestFinalizeMessage:
question="q",
decoded_token={"sub": user},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -477,10 +477,10 @@ class TestFinalizeMessage:
def test_finalize_strips_null_bytes(self, pg_conn):
"""A NUL-laden tool result (07-17 PDF incident) must not kill the
conversation save — Postgres rejects \\x00 in text and jsonb."""
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
MessageUpdateOutcome,
)
@@ -515,10 +515,10 @@ class TestFinalizeMessage:
assert msgs[0]["metadata"] == {"key": "val"}
def test_finalizes_failed_records_error(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -531,7 +531,7 @@ class TestFinalizeMessage:
decoded_token={"sub": user},
)
err = RuntimeError("provider down")
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -552,7 +552,7 @@ class TestFinalizeMessage:
rows as 'confirmed' for the same message_id."""
from sqlalchemy import text as sql_text
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -572,7 +572,7 @@ class TestFinalizeMessage:
),
{"cid": "c1", "mid": res["message_id"]},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -586,10 +586,10 @@ class TestFinalizeMessage:
assert status == "confirmed"
def test_finalize_returns_not_found_for_unknown_message(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
with _patch_db(pg_conn):
@@ -614,10 +614,10 @@ class TestFinalizeMessage:
from sqlalchemy import text as sql_text
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories import conversations as conv_module
from docsgpt.storage.db.repositories import conversations as conv_module
user = "u-fin-rollback"
@@ -632,10 +632,10 @@ class TestFinalizeMessage:
raise
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_savepoint_session,
), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_savepoint_session,
):
svc = ConversationService()
@@ -686,10 +686,10 @@ class TestFinalizeMessage:
assert msg_status == "pending"
def test_finalize_generates_title_when_provided(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -703,7 +703,7 @@ class TestFinalizeMessage:
question="long question that becomes the fallback name",
decoded_token={"sub": user},
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
MessageUpdateOutcome,
)
assert svc.finalize_message(
@@ -730,10 +730,10 @@ class TestSaveUserQuestionFinalizeFailedFlow:
"""LLM fails immediately; question stays queryable with status='failed' + error metadata."""
def test_failed_llm_leaves_question_persisted(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -769,10 +769,10 @@ class TestSaveUserQuestionFinalizeFailedFlow:
class TestCompressionMetadata:
def test_update_compression_metadata(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -792,7 +792,7 @@ class TestCompressionMetadata:
)
def test_update_compression_raises_on_error(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -802,13 +802,13 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_broken,
), pytest.raises(RuntimeError):
ConversationService().update_compression_metadata("abc", {})
def test_append_compression_message_skips_empty_summary(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -818,10 +818,10 @@ class TestCompressionMetadata:
)
def test_append_compression_message_appends_summary(self, pg_conn):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -844,7 +844,7 @@ class TestCompressionMetadata:
assert any(m["response"] == "A summary" for m in messages)
def test_append_compression_message_swallows_error(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -854,7 +854,7 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_session",
"docsgpt.api.answer.services.conversation_service.db_session",
_broken,
):
ConversationService().append_compression_message(
@@ -864,7 +864,7 @@ class TestCompressionMetadata:
def test_get_compression_metadata_returns_none_for_missing(
self, pg_conn,
):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
with _patch_db(pg_conn):
@@ -881,7 +881,7 @@ class TestCompressionMetadata:
would raise and pollute logs with a stack trace every call."""
import logging
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -896,7 +896,7 @@ class TestCompressionMetadata:
)
def test_get_compression_metadata_handles_exception(self):
from application.api.answer.services.conversation_service import (
from docsgpt.api.answer.services.conversation_service import (
ConversationService,
)
@@ -906,7 +906,7 @@ class TestCompressionMetadata:
yield
with patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
_broken,
):
got = ConversationService().get_compression_metadata("abc")
@@ -2,7 +2,7 @@
import pytest
from application.api.answer.services.persistence_policy import (
from docsgpt.api.answer.services.persistence_policy import (
VISIBILITY_HIDDEN,
VISIBILITY_LISTED,
resolve_persistence,
@@ -5,7 +5,7 @@ import pytest
class TestTemplateEngine:
def test_render_simple_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("Hello {{ name }}", {"name": "World"})
@@ -13,7 +13,7 @@ class TestTemplateEngine:
assert result == "Hello World"
def test_render_with_namespace(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
context = {
@@ -27,7 +27,7 @@ class TestTemplateEngine:
assert result == "Alice is a admin on 2025-10-22"
def test_render_empty_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("", {"key": "value"})
@@ -35,7 +35,7 @@ class TestTemplateEngine:
assert result == ""
def test_render_template_without_variables(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
result = engine.render("Just plain text", {})
@@ -43,7 +43,7 @@ class TestTemplateEngine:
assert result == "Just plain text"
def test_render_undefined_variable_returns_empty_string(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
@@ -51,7 +51,7 @@ class TestTemplateEngine:
assert result == "Hello "
def test_render_syntax_error_raises_error(self):
from application.templates.template_engine import (
from docsgpt.templates.template_engine import (
TemplateEngine,
TemplateRenderError,
)
@@ -62,7 +62,7 @@ class TestTemplateEngine:
engine.render("Hello {{ name", {"name": "World"})
def test_render_blocks_unsafe_attribute_access(self):
from application.templates.template_engine import (
from docsgpt.templates.template_engine import (
TemplateEngine,
TemplateRenderError,
)
@@ -74,25 +74,25 @@ class TestTemplateEngine:
engine.render(payload, {})
def test_validate_template_valid(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("Valid {{ variable }}") is True
def test_validate_template_invalid(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("Invalid {{ variable") is False
def test_validate_empty_template(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
assert engine.validate_template("") is True
def test_extract_variables(self):
from application.templates.template_engine import TemplateEngine
from docsgpt.templates.template_engine import TemplateEngine
engine = TemplateEngine()
template = "{{ user.name }} and {{ user.email }}"
@@ -106,7 +106,7 @@ class TestTemplateEngine:
class TestSystemNamespace:
def test_system_namespace_build(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build(
@@ -120,7 +120,7 @@ class TestSystemNamespace:
assert "timestamp" in context
def test_system_namespace_generates_request_id(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build(user_id="user_123")
@@ -129,13 +129,13 @@ class TestSystemNamespace:
assert len(context["request_id"]) > 0
def test_system_namespace_name(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
assert builder.namespace_name == "system"
def test_system_namespace_date_format(self):
from application.templates.namespaces import SystemNamespace
from docsgpt.templates.namespaces import SystemNamespace
builder = SystemNamespace()
context = builder.build()
@@ -150,7 +150,7 @@ class TestSystemNamespace:
class TestPassthroughNamespace:
def test_passthrough_namespace_build(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {"company": "Acme", "user_name": "John", "count": 42}
@@ -162,7 +162,7 @@ class TestPassthroughNamespace:
assert context["count"] == 42
def test_passthrough_namespace_empty(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
context = builder.build(passthrough_data=None)
@@ -170,7 +170,7 @@ class TestPassthroughNamespace:
assert context == {}
def test_passthrough_namespace_filters_unsafe_values(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {
@@ -190,7 +190,7 @@ class TestPassthroughNamespace:
assert "unsafe_list" not in context
def test_passthrough_namespace_allows_none_values(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
passthrough_data = {"nullable_field": None}
@@ -200,7 +200,7 @@ class TestPassthroughNamespace:
assert context["nullable_field"] is None
def test_passthrough_namespace_name(self):
from application.templates.namespaces import PassthroughNamespace
from docsgpt.templates.namespaces import PassthroughNamespace
builder = PassthroughNamespace()
assert builder.namespace_name == "passthrough"
@@ -210,7 +210,7 @@ class TestPassthroughNamespace:
class TestSourceNamespace:
def test_source_namespace_build_with_docs(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs = [
@@ -227,7 +227,7 @@ class TestSourceNamespace:
assert context["summaries"] == docs_together
def test_source_namespace_build_empty(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
context = builder.build(docs=None, docs_together=None)
@@ -235,7 +235,7 @@ class TestSourceNamespace:
assert context == {}
def test_source_namespace_build_docs_only(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs = [{"text": "Doc 1"}]
@@ -247,7 +247,7 @@ class TestSourceNamespace:
assert "content" not in context
def test_source_namespace_build_docs_together_only(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
docs_together = "Content here"
@@ -259,7 +259,7 @@ class TestSourceNamespace:
assert "documents" not in context
def test_source_namespace_name(self):
from application.templates.namespaces import SourceNamespace
from docsgpt.templates.namespaces import SourceNamespace
builder = SourceNamespace()
assert builder.namespace_name == "source"
@@ -269,7 +269,7 @@ class TestSourceNamespace:
class TestToolsNamespace:
def test_tools_namespace_build_with_memory_data(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
tools_data = {
@@ -282,7 +282,7 @@ class TestToolsNamespace:
assert context["memory"]["available"] is True
def test_tools_namespace_build_empty(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
context = builder.build(tools_data=None)
@@ -290,7 +290,7 @@ class TestToolsNamespace:
assert context == {}
def test_tools_namespace_build_multiple_tools(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
tools_data = {
@@ -309,7 +309,7 @@ class TestToolsNamespace:
assert context["api"]["status"] == "success"
def test_tools_namespace_filters_unsafe_values(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
@@ -324,13 +324,13 @@ class TestToolsNamespace:
assert "unsafe_tool" not in context
def test_tools_namespace_name(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
assert builder.namespace_name == "tools"
def test_tools_namespace_with_empty_dict(self):
from application.templates.namespaces import ToolsNamespace
from docsgpt.templates.namespaces import ToolsNamespace
builder = ToolsNamespace()
context = builder.build(tools_data={})
@@ -342,7 +342,7 @@ class TestToolsNamespace:
class TestNamespaceManagerWithTools:
def test_namespace_manager_includes_tools_in_context(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
tools_data = {"memory": {"root": "content", "available": True}}
@@ -353,7 +353,7 @@ class TestNamespaceManagerWithTools:
assert context["tools"]["memory"]["root"] == "content"
def test_namespace_manager_build_context_all_namespaces(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
context = manager.build_context(
@@ -371,7 +371,7 @@ class TestNamespaceManagerWithTools:
assert context["tools"]["memory"]["root"] == "notes"
def test_namespace_manager_build_context_partial_data(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
context = manager.build_context(request_id="req_123")
@@ -386,13 +386,13 @@ class TestPromptRendererArtifactParent:
def test_render_resolves_artifact_by_id_with_conversation_parent(self):
from unittest.mock import patch
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
row = {"id": "art-1", "current_version": 1, "kind": "document", "title": "Report"}
version = {"mime_type": "text/html", "filename": "report.html", "size": 12}
with patch(
"application.storage.db.repositories.artifacts.ArtifactsRepository"
) as repo_cls, patch("application.storage.db.session.db_readonly"):
"docsgpt.storage.db.repositories.artifacts.ArtifactsRepository"
) as repo_cls, patch("docsgpt.storage.db.session.db_readonly"):
repo = repo_cls.return_value
repo.get_artifact_in_parent.return_value = row
repo.get_version.return_value = version
@@ -407,7 +407,7 @@ class TestPromptRendererArtifactParent:
assert kwargs.get("conversation_id") == "conv-1"
def test_render_without_parent_yields_empty_lookup(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
# No artifact_parent (e.g. a first-turn conversation_id is None) must not error;
# the lookup returns an empty mapping and the attribute renders blank.
@@ -418,7 +418,7 @@ class TestPromptRendererArtifactParent:
assert rendered == "[]"
def test_namespace_manager_get_builder(self):
from application.templates.namespaces import NamespaceManager, SystemNamespace
from docsgpt.templates.namespaces import NamespaceManager, SystemNamespace
manager = NamespaceManager()
builder = manager.get_builder("system")
@@ -426,7 +426,7 @@ class TestPromptRendererArtifactParent:
assert isinstance(builder, SystemNamespace)
def test_namespace_manager_get_builder_nonexistent(self):
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
builder = manager.get_builder("nonexistent")
@@ -436,7 +436,7 @@ class TestPromptRendererArtifactParent:
def test_namespace_manager_handles_builder_exceptions(self):
from unittest.mock import patch
from application.templates.namespaces import NamespaceManager
from docsgpt.templates.namespaces import NamespaceManager
manager = NamespaceManager()
@@ -456,7 +456,7 @@ class TestPromptRendererArtifactParent:
class TestPromptRenderer:
def test_render_prompt_with_template_syntax(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ system.user_id }}, today is {{ system.date }}"
@@ -467,7 +467,7 @@ class TestPromptRenderer:
assert "202" in result
def test_render_prompt_with_passthrough_data(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Company: {{ passthrough.company }}\nUser: {{ passthrough.user_name }}"
@@ -479,7 +479,7 @@ class TestPromptRenderer:
assert "User: John" in result
def test_render_prompt_with_source_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use this information:\n{{ source.content }}"
@@ -491,7 +491,7 @@ class TestPromptRenderer:
assert "Important document content" in result
def test_render_prompt_empty_content(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
result = renderer.render_prompt("")
@@ -499,7 +499,7 @@ class TestPromptRenderer:
assert result == ""
def test_render_prompt_legacy_format_with_summaries(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Context: {summaries}\nQuestion: What is this?"
@@ -510,7 +510,7 @@ class TestPromptRenderer:
assert "Context: This is the document content" in result
def test_render_prompt_legacy_format_without_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Context: {summaries}\nQuestion: What is this?"
@@ -522,7 +522,7 @@ class TestPromptRenderer:
assert "Question: What is this?" in result
def test_render_prompt_combined_namespace_variables(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "User: {{ passthrough.user }}, Date: {{ system.date }}, Docs: {{ source.content }}"
@@ -540,7 +540,7 @@ class TestPromptRenderer:
assert "Doc content" in result
def test_render_prompt_with_tools_data(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Memory contents:\n{{ tools.memory.root }}\n\nStatus: {{ tools.memory.available }}"
@@ -557,7 +557,7 @@ class TestPromptRenderer:
assert "Status: True" in result
def test_render_prompt_with_all_namespaces(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = """
@@ -583,7 +583,7 @@ Memory: {{ tools.memory.root }}
assert "Notes content" in result
def test_render_prompt_undefined_variable_returns_empty_string(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ undefined_var }}"
@@ -592,7 +592,7 @@ Memory: {{ tools.memory.root }}
assert result == "Hello "
def test_render_prompt_with_undefined_variable_in_template(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Hello {{ undefined_name }}"
@@ -601,19 +601,19 @@ Memory: {{ tools.memory.root }}
assert result == "Hello "
def test_validate_template_valid(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
assert renderer.validate_template("Valid {{ variable }}") is True
def test_validate_template_invalid(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
assert renderer.validate_template("Invalid {{ variable") is False
def test_extract_variables(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
template = "{{ var1 }} and {{ var2 }}"
@@ -623,7 +623,7 @@ Memory: {{ tools.memory.root }}
assert isinstance(result, set)
def test_uses_template_syntax_detection(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
@@ -632,7 +632,7 @@ Memory: {{ tools.memory.root }}
assert renderer._uses_template_syntax("Plain text") is False
def test_apply_legacy_substitutions(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use {summaries} to answer"
@@ -643,7 +643,7 @@ Memory: {{ tools.memory.root }}
assert "Use Important info to answer" in result
def test_apply_legacy_substitutions_without_docs(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "Use {summaries} to answer"
@@ -658,7 +658,7 @@ Memory: {{ tools.memory.root }}
class TestPromptRendererIntegration:
def test_render_prompt_real_world_scenario(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = "You are helping {{ passthrough.company }}.\n\nUser: {{ passthrough.user_name }}\n\nRequest ID: {{ system.request_id }}\n\nDate: {{ system.date }}\n\nReference Documents:\n\n{{ source.content }}\n\nPlease answer the question professionally."
@@ -681,7 +681,7 @@ class TestPromptRendererIntegration:
assert "professionally" in result
def test_render_prompt_multiple_doc_references(self):
from application.api.answer.services.prompt_renderer import PromptRenderer
from docsgpt.api.answer.services.prompt_renderer import PromptRenderer
renderer = PromptRenderer()
prompt = """Documents: {{ source.content }} \n\nAlso summaries: {{ source.summaries }}"""
@@ -700,7 +700,7 @@ class TestPromptRendererIntegration:
class TestStreamProcessorPromptRendering:
def test_stream_processor_pre_fetch_docs_none_doc_mode(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test question", "isNoneDoc": True}
processor = StreamProcessor(request_data, None)
@@ -711,8 +711,8 @@ class TestStreamProcessorPromptRendering:
assert docs_list is None
def test_pre_fetch_tools_disabled_globally(self, monkeypatch):
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
monkeypatch.setattr(settings, "ENABLE_TOOL_PREFETCH", False)
@@ -724,7 +724,7 @@ class TestStreamProcessorPromptRendering:
assert result is None
def test_pre_fetch_tools_disabled_per_request(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "test", "disable_tool_prefetch": True}
processor = StreamProcessor(request_data, {"sub": "user1"})
@@ -736,8 +736,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_skips_tool_with_no_actions(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -753,7 +753,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -773,8 +773,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_enabled_by_default(self, mock_mongo_db, monkeypatch):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -790,7 +790,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -812,7 +812,7 @@ class TestStreamProcessorPromptRendering:
assert "memory_ls" in result["memory"]
def test_pre_fetch_tools_no_tools_configured(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "test"}
processor = StreamProcessor(request_data, {"sub": "user1"})
@@ -824,8 +824,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_memory_returns_error(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -841,7 +841,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -864,8 +864,8 @@ class TestStreamProcessorPromptRendering:
def test_pre_fetch_tools_memory_returns_empty(self, mock_mongo_db):
from unittest.mock import MagicMock, patch
from application.api.answer.services.stream_processor import StreamProcessor
from application.core.settings import settings
from docsgpt.api.answer.services.stream_processor import StreamProcessor
from docsgpt.core.settings import settings
db = mock_mongo_db[settings.MONGO_DB_NAME]
db["user_tools"].insert_one(
@@ -881,7 +881,7 @@ class TestStreamProcessorPromptRendering:
processor = StreamProcessor(request_data, {"sub": "user1"})
with patch(
"application.agents.tools.tool_manager.ToolManager"
"docsgpt.agents.tools.tool_manager.ToolManager"
) as mock_manager_class:
mock_manager = MagicMock()
mock_manager_class.return_value = mock_manager
@@ -1,4 +1,4 @@
"""Tests for application/api/answer/services/stream_processor.py.
"""Tests for docsgpt/api/answer/services/stream_processor.py.
The previous suite was tightly coupled to Mongo (mock_mongo_db fixture,
bson.ObjectId, bson.DBRef, find_one, etc.) which no longer exist after the
@@ -24,7 +24,7 @@ class TestStreamProcessorInitialization:
pass
def test_initializes_with_decoded_token(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
conv_id = _STATIC_OID
request_data = {
@@ -41,7 +41,7 @@ class TestStreamProcessorInitialization:
assert processor.conversation_id == request_data["conversation_id"]
def test_initializes_without_token(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test question"}
@@ -52,7 +52,7 @@ class TestStreamProcessorInitialization:
assert processor.data == request_data
def test_initializes_default_attributes(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor({"question": "Test"}, {"sub": "user_123"})
@@ -66,7 +66,7 @@ class TestStreamProcessorInitialization:
assert processor.shared_token is None
def test_extracts_conversation_id_from_request(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
conv_id = _STATIC_OID
request_data = {"question": "Test", "conversation_id": conv_id}
@@ -81,7 +81,7 @@ class TestStreamProcessorHistoryLoading:
pass
def test_uses_request_history_when_no_conversation_id(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "What is Python?",
@@ -98,7 +98,7 @@ class TestStreamProcessorAgentConfiguration:
pass
def test_uses_default_config_without_api_key(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Test"}
@@ -110,7 +110,7 @@ class TestStreamProcessorAgentConfiguration:
def test_embedded_workflow_without_saved_id(self):
"""A preview run with no saved workflow id carries no ``workflow_id``."""
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "Test",
@@ -126,7 +126,7 @@ class TestStreamProcessorAgentConfiguration:
def test_embedded_workflow_with_saved_id_persists_run(self):
"""A saved workflow id alongside the embedded graph is captured so the
run can persist a ``workflow_runs`` row for artifact listing."""
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {
"question": "Test",
@@ -152,7 +152,7 @@ class TestStreamProcessorDocPrefetch:
def test_prefetch_skipped_when_no_active_docs(self):
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor(
{"question": "Hi there"},
@@ -170,7 +170,7 @@ class TestStreamProcessorDocPrefetch:
def test_prefetch_skipped_when_active_docs_is_default(self):
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor(
{"question": "Hi", "active_docs": "default"},
@@ -194,7 +194,7 @@ class TestStreamProcessorAttachments:
pass
def test_handles_empty_attachments(self):
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
request_data = {"question": "Simple question"}
@@ -225,7 +225,7 @@ class TestBuildContinuationFromMessages:
def _make_processor():
from unittest.mock import MagicMock
from application.api.answer.services.stream_processor import StreamProcessor
from docsgpt.api.answer.services.stream_processor import StreamProcessor
processor = StreamProcessor({"question": ""}, {"sub": "user_123"})
fake_agent = MagicMock()
@@ -1,4 +1,4 @@
"""Targeted tests for application/api/answer/services/stream_processor.py.
"""Targeted tests for docsgpt/api/answer/services/stream_processor.py.
Tests the ``get_prompt`` helper and simpler StreamProcessor methods against
real ephemeral Postgres.
@@ -9,7 +9,7 @@ from unittest.mock import patch
import pytest
import application.api.answer.services.stream_processor as sp_mod
import docsgpt.api.answer.services.stream_processor as sp_mod
@contextmanager
@@ -19,46 +19,46 @@ def _patch_db(conn):
yield conn
with patch(
"application.api.answer.services.stream_processor.db_readonly", _yield
"docsgpt.api.answer.services.stream_processor.db_readonly", _yield
), patch(
"application.api.answer.services.stream_processor.db_session", _yield
"docsgpt.api.answer.services.stream_processor.db_session", _yield
):
yield
class TestGetPrompt:
def test_default_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("default")
assert isinstance(got, str)
assert len(got) > 0
def test_creative_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("creative")
assert isinstance(got, str) and len(got) > 0
def test_strict_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("strict")
assert isinstance(got, str) and len(got) > 0
def test_agentic_default_preset(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt("agentic_default")
assert isinstance(got, str) and len(got) > 0
def test_none_defaults_to_default(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
got = get_prompt(None)
assert isinstance(got, str) and len(got) > 0
def test_empty_string_defaults_to_default(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
assert get_prompt("") == get_prompt("default")
def test_non_string_id_converted(self):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
# A UUID object would be stringified; use an int to test the branch
with pytest.raises(ValueError):
# Int converts to str "42" which isn't a preset, and will
@@ -66,18 +66,18 @@ class TestGetPrompt:
get_prompt(42)
def test_unknown_prompt_id_raises(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
with _patch_db(pg_conn), pytest.raises(ValueError):
get_prompt("00000000-0000-0000-0000-000000000000")
def test_legacy_id_unknown_raises(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from docsgpt.api.answer.services.stream_processor import get_prompt
with _patch_db(pg_conn), pytest.raises(ValueError):
get_prompt("507f1f77bcf86cd799439011")
def test_uuid_lookup_returns_content(self, pg_conn):
from application.api.answer.services.stream_processor import get_prompt
from application.storage.db.repositories.prompts import (
from docsgpt.api.answer.services.stream_processor import get_prompt
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -91,7 +91,7 @@ class TestGetPrompt:
class TestStreamProcessorInit:
def test_basic_init(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
data = {"question": "hi", "conversation_id": "conv-1"}
@@ -103,7 +103,7 @@ class TestStreamProcessorInit:
assert sp.conversation_id == "conv-1"
def test_init_no_token(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "hi"}, None)
@@ -112,7 +112,7 @@ class TestStreamProcessorInit:
assert sp.conversation_id is None
def test_init_sets_agent_id_from_data(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
data = {"question": "hi", "agent_id": "agent-xyz"}
@@ -122,7 +122,7 @@ class TestStreamProcessorInit:
class TestLoadConversationHistory:
def test_no_conversation_id_uses_request_history(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
import json as _json
@@ -136,10 +136,10 @@ class TestLoadConversationHistory:
assert len(sp.history) == 1
def test_loads_existing_conversation_history(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -162,7 +162,7 @@ class TestLoadConversationHistory:
)
# Also patch conversation_service.get_conversation's DB accessor
with _patch_db(pg_conn), patch(
"application.api.answer.services.conversation_service.db_readonly",
"docsgpt.api.answer.services.conversation_service.db_readonly",
) as mock_readonly:
@contextmanager
def _yield():
@@ -172,10 +172,10 @@ class TestLoadConversationHistory:
assert len(sp.history) == 2
def test_unauthorized_conversation_raises(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.conversations import (
from docsgpt.storage.db.repositories.conversations import (
ConversationsRepository,
)
@@ -186,7 +186,7 @@ class TestLoadConversationHistory:
{"sub": "hacker"},
)
with _patch_db(pg_conn), patch(
"application.api.answer.services.conversation_service.db_readonly"
"docsgpt.api.answer.services.conversation_service.db_readonly"
) as mock_readonly:
@contextmanager
def _yield():
@@ -198,7 +198,7 @@ class TestLoadConversationHistory:
class TestHasActiveDocs:
def test_false_when_no_active_docs(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -207,7 +207,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is False
def test_true_when_source_active(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -215,7 +215,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is True
def test_false_when_source_empty(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -223,7 +223,7 @@ class TestHasActiveDocs:
assert sp._has_active_docs() is False
def test_default_returns_false(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -234,7 +234,7 @@ class TestHasActiveDocs:
class TestProcessAttachments:
def test_no_attachments(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -244,10 +244,10 @@ class TestProcessAttachments:
assert sp.attachments == []
def test_retrieves_attachments_by_id(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.attachments import (
from docsgpt.storage.db.repositories.attachments import (
AttachmentsRepository,
)
@@ -269,7 +269,7 @@ class TestProcessAttachments:
class TestGetAttachmentsContent:
def test_empty_list(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -278,7 +278,7 @@ class TestGetAttachmentsContent:
assert got == []
def test_skips_missing_attachments(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"question": "q"}, {"sub": "u"})
@@ -291,14 +291,14 @@ class TestGetAttachmentsContent:
class TestResolveAgentId:
def test_returns_agent_id_from_request(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"agent_id": "req-agent"}, {"sub": "u"})
assert sp._resolve_agent_id() == "req-agent"
def test_returns_none_if_not_set(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -307,7 +307,7 @@ class TestResolveAgentId:
class TestGetAgentKey:
def test_returns_tuple_for_none_agent_id(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -315,7 +315,7 @@ class TestGetAgentKey:
assert key is None and is_shared is False and tok is None
def test_raises_for_missing_agent(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -325,10 +325,10 @@ class TestGetAgentKey:
)
def test_returns_key_for_owned_agent(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "a", "published", key="the-key",
@@ -340,10 +340,10 @@ class TestGetAgentKey:
assert shared is False
def test_raises_on_unauthorized_access(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
agent = AgentsRepository(pg_conn).create(
"owner", "a", "published", key="k", shared=False,
@@ -372,7 +372,7 @@ def _stub_db_readonly(monkeypatch, sp_mod):
class TestConfigureSource:
def test_agent_data_with_sources_list(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -386,7 +386,7 @@ class TestConfigureSource:
assert sp.source == {"active_docs": ["s1"]}
def test_agent_data_with_single_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -395,7 +395,7 @@ class TestConfigureSource:
assert sp.source == {"active_docs": "src-1"}
def test_agent_data_default_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -449,7 +449,7 @@ class TestConfigureSource:
assert sp.source == {}
def test_request_active_docs_default(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"active_docs": "default"}, {"sub": "u"})
@@ -457,7 +457,7 @@ class TestConfigureSource:
assert sp.source == {}
def test_no_data_empty_source(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -467,7 +467,7 @@ class TestConfigureSource:
class TestConfigureRetriever:
def test_defaults(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -476,7 +476,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_agent_overrides(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -486,7 +486,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 5
def test_agent_wins_over_request_on_agent_bound(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -498,7 +498,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 5
def test_body_wins_on_agentless(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -510,7 +510,7 @@ class TestConfigureRetriever:
def test_agent_bound_drops_body_chunks_and_retriever(self):
# Missing agent values fall back to system defaults, not body's.
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor(
@@ -522,7 +522,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_invalid_agent_chunks_falls_back(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -531,7 +531,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_invalid_request_chunks_falls_back(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"chunks": "abc"}, {"sub": "u"})
@@ -539,7 +539,7 @@ class TestConfigureRetriever:
assert sp.retriever_config["chunks"] == 2
def test_isnonedoc_without_api_key_sets_chunks_to_0(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"isNoneDoc": True}, {"sub": "u"})
@@ -550,10 +550,10 @@ class TestConfigureRetriever:
class TestGetPromptContent:
def test_gets_from_agent_config(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.prompts import (
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -573,10 +573,10 @@ class TestGetPromptContent:
assert "{{ system.persona }}" in content
def test_templated_custom_prompt_is_left_alone(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.prompts import (
from docsgpt.storage.db.repositories.prompts import (
PromptsRepository,
)
@@ -592,7 +592,7 @@ class TestGetPromptContent:
assert sp._persona is None
def test_returns_none_on_missing(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -605,7 +605,7 @@ class TestGetPromptContent:
assert content is None
def test_caches_prompt_content(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -614,7 +614,7 @@ class TestGetPromptContent:
assert sp._get_prompt_content() == "cached"
def test_agentic_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -624,7 +624,7 @@ class TestGetPromptContent:
assert "source.summaries" not in content
def test_research_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -633,7 +633,7 @@ class TestGetPromptContent:
assert "`search` tool" in content
def test_classic_agent_gets_classic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -644,7 +644,7 @@ class TestGetPromptContent:
assert "documents are provided with the question" in content
def test_null_prompt_id_agentic_agent_gets_agentic_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
# PG ``agents.prompt_id`` is NULL for agents that never chose a
@@ -657,7 +657,7 @@ class TestGetPromptContent:
assert "source.summaries" not in content
def test_null_prompt_id_classic_agent_gets_default_preset(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -671,7 +671,7 @@ class TestGetPromptContent:
class TestPreFetchDocs:
def test_skips_when_no_active_docs(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
@@ -680,7 +680,7 @@ class TestPreFetchDocs:
assert docs is None and raw is None
def test_skips_when_isnonedoc_no_agent(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"isNoneDoc": True}, {"sub": "u"})
@@ -690,13 +690,13 @@ class TestPreFetchDocs:
assert docs is None and raw is None
def test_handles_retriever_exception(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
sp.source = {"active_docs": "src"}
with patch(
"application.api.answer.services.stream_processor.StreamProcessor.create_retriever",
"docsgpt.api.answer.services.stream_processor.StreamProcessor.create_retriever",
side_effect=RuntimeError("boom"),
):
docs, raw = sp.pre_fetch_docs("q")
@@ -705,48 +705,48 @@ class TestPreFetchDocs:
class TestPreFetchTools:
def test_disabled_globally_returns_none(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "u"})
with patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
False,
):
got = sp.pre_fetch_tools()
assert got is None
def test_disabled_per_request(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"disable_tool_prefetch": True}, {"sub": "u"})
with patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
assert got is None
def test_no_template_skips_default_tool_prefetch(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({}, {"sub": "no-tools-user"})
sp._prompt_content = "No template syntax here"
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
assert got is None
def test_unresolvable_prompt_prefetches_only_explicit_rows(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -766,7 +766,7 @@ class TestPreFetchTools:
return {"ok": True}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -778,8 +778,8 @@ class TestPreFetchTools:
def test_default_tool_prefetched_when_template_references_it(
self, pg_conn
):
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -792,7 +792,7 @@ class TestPreFetchTools:
return {"ok": True}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -807,11 +807,11 @@ class TestPreFetchTools:
assert got.get("read_webpage") == {"ok": True}
def test_explicit_row_keeps_name_key_over_default(self, pg_conn):
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -825,7 +825,7 @@ class TestPreFetchTools:
return {"is_default": bool(tool_doc.get("default"))}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -838,8 +838,8 @@ class TestPreFetchTools:
def test_fetch_tool_data_executes_referenced_memory_view(self, pg_conn):
from unittest.mock import MagicMock
from application.agents.default_tools import synthesize_default_tool
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import synthesize_default_tool
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
@@ -853,7 +853,7 @@ class TestPreFetchTools:
mock_manager.load_tool.return_value = mock_tool
with patch(
"application.agents.tools.tool_manager.ToolManager",
"docsgpt.agents.tools.tool_manager.ToolManager",
return_value=mock_manager,
):
got = sp._fetch_tool_data(tool_doc, {"memory_view"})
@@ -864,14 +864,14 @@ class TestPreFetchTools:
mock_tool.execute_action.assert_called_once_with("memory_view")
def test_agent_bound_invocation_omits_default_tool_prefetch(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"agent_id": "agent-xyz"}, {"sub": "u-ag"})
sp._required_tool_actions = {"read_webpage": {None}}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
):
got = sp.pre_fetch_tools()
@@ -881,11 +881,11 @@ class TestPreFetchTools:
"""An explicit row and the synthesized default of the same name
coexist: name key stays on the explicit, default reachable by
synthetic id only."""
from application.agents.default_tools import default_tool_id
from application.api.answer.services.stream_processor import (
from docsgpt.agents.default_tools import default_tool_id
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.user_tools import (
from docsgpt.storage.db.repositories.user_tools import (
UserToolsRepository,
)
@@ -906,7 +906,7 @@ class TestPreFetchTools:
}
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
"docsgpt.api.answer.services.stream_processor.settings.ENABLE_TOOL_PREFETCH",
True,
), patch.object(sp, "_fetch_tool_data", _fake_fetch):
got = sp.pre_fetch_tools()
@@ -921,7 +921,7 @@ class TestValidateAndSetModelAgentAuthority:
"""Agent-bound chats: agent's ``default_model_id`` is authoritative."""
def test_agent_bound_ignores_body_model_id(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "caller"})
@@ -937,10 +937,10 @@ class TestValidateAndSetModelAgentAuthority:
return True
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
side_effect=_fake_validate,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="global-default",
):
sp._validate_and_set_model()
@@ -950,17 +950,17 @@ class TestValidateAndSetModelAgentAuthority:
assert ("agent-model", "owner") in captured
def test_agent_bound_no_default_falls_back_to_system(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "u"})
sp._agent_data = {"user": "u"}
sp.agent_config = {"default_model_id": "", "user_id": "u"}
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=False,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="global-default",
):
sp._validate_and_set_model()
@@ -968,13 +968,13 @@ class TestValidateAndSetModelAgentAuthority:
assert sp.model_user_id is None
def test_agentless_body_model_still_wins(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
sp = StreamProcessor({"model_id": "body-model"}, {"sub": "u"})
sp._agent_data = None
with patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=True,
):
sp._validate_and_set_model()
@@ -986,14 +986,14 @@ class TestGetDataFromApiKeySourceUnion:
"""`_get_data_from_api_key`: primary ∪ extras, deduplicated, primary first."""
def _make_sp(self):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
return StreamProcessor({}, {"sub": "u"})
def test_union_primary_and_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-both"
sources_repo = SourcesRepository(pg_conn)
@@ -1023,8 +1023,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] == str(primary["id"])
def test_only_primary(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-primary-only"
primary = SourcesRepository(pg_conn).create(
@@ -1045,8 +1045,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] == str(primary["id"])
def test_only_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-extras-only"
e1 = SourcesRepository(pg_conn).create(name="e1", user_id=owner)
@@ -1067,8 +1067,8 @@ class TestGetDataFromApiKeySourceUnion:
assert data["source"] is None
def test_dedupe_primary_repeated_in_extras(self, pg_conn):
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-merge-dedupe"
primary = SourcesRepository(pg_conn).create(
@@ -1096,11 +1096,11 @@ class TestAgentBoundFieldsAuthoritative:
"""End-to-end regression: agent's source/model/chunks/retriever win."""
def test_agent_values_win_over_body(self, pg_conn):
from application.api.answer.services.stream_processor import (
from docsgpt.api.answer.services.stream_processor import (
StreamProcessor,
)
from application.storage.db.repositories.agents import AgentsRepository
from application.storage.db.repositories.sources import SourcesRepository
from docsgpt.storage.db.repositories.agents import AgentsRepository
from docsgpt.storage.db.repositories.sources import SourcesRepository
owner = "u-regr-agent-authority"
primary = SourcesRepository(pg_conn).create(
@@ -1130,10 +1130,10 @@ class TestAgentBoundFieldsAuthoritative:
sp = StreamProcessor(body, {"sub": owner})
with _patch_db(pg_conn), patch(
"application.api.answer.services.stream_processor.validate_model_id",
"docsgpt.api.answer.services.stream_processor.validate_model_id",
return_value=True,
), patch(
"application.api.answer.services.stream_processor.get_default_model_id",
"docsgpt.api.answer.services.stream_processor.get_default_model_id",
return_value="system-default",
):
sp._configure_agent()
@@ -1,6 +1,6 @@
"""Token-usage attribution tests for the always-on inline-persist model.
Persistence is owned by the per-call decorator in ``application.usage``.
Persistence is owned by the per-call decorator in ``docsgpt.usage``.
``finalize_message`` no longer writes ``token_usage`` rows. These tests
exercise the decorator path through ``stream_token_usage`` /
``gen_token_usage``:
@@ -63,7 +63,7 @@ class TestDecoratorAlwaysPersists:
"""Per-call inline persistence — no opt-in flag."""
def test_primary_stream_writes_agent_stream_row(self, pg_conn):
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
@@ -81,7 +81,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk-b"
llm = _PrimaryLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
for _ in llm._raw(
"m", [{"role": "user", "content": "hi"}], True, None,
):
@@ -102,7 +102,7 @@ class TestDecoratorAlwaysPersists:
def test_side_channel_source_flows_to_row(self, pg_conn):
"""``_token_usage_source`` overrides the default ``agent_stream``."""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
@@ -120,7 +120,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk"
llm = _RagLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass
@@ -137,7 +137,7 @@ class TestDecoratorAlwaysPersists:
"""``_request_id`` on the LLM (stamped by the route) lands in
``token_usage.request_id`` so ``count_in_range`` can DISTINCT it.
"""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
user_id = _seed_user(pg_conn)
request_id = f"req-{uuid.uuid4().hex[:12]}"
@@ -156,7 +156,7 @@ class TestDecoratorAlwaysPersists:
yield "chunk"
llm = _PrimaryLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
# Call twice — the route invokes the LLM once per tool round.
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass
@@ -173,7 +173,7 @@ class TestDecoratorAlwaysPersists:
assert all(r[0] == request_id for r in rows)
def test_zero_count_call_is_skipped(self, pg_conn):
from application.usage import gen_token_usage
from docsgpt.usage import gen_token_usage
user_id = _seed_user(pg_conn)
@@ -190,7 +190,7 @@ class TestDecoratorAlwaysPersists:
return None # empty result → 0 generated tokens, 0 prompt tokens
llm = _EmptyLLM()
with _patch_db_session_for(("application.usage",), pg_conn):
with _patch_db_session_for(("docsgpt.usage",), pg_conn):
llm._raw("m", [], False, None)
n = pg_conn.execute(
@@ -206,7 +206,7 @@ class TestDecoratorAlwaysPersists:
constraint; the decorator skips before that to keep the stream
running.
"""
from application.usage import stream_token_usage
from docsgpt.usage import stream_token_usage
class _OrphanLLM:
decoded_token = None
@@ -222,8 +222,8 @@ class TestDecoratorAlwaysPersists:
llm = _OrphanLLM()
with _patch_db_session_for(
("application.usage",), pg_conn,
), caplog.at_level(logging.WARNING, logger="application.usage"):
("docsgpt.usage",), pg_conn,
), caplog.at_level(logging.WARNING, logger="docsgpt.usage"):
for _ in llm._raw("m", [{"role": "user", "content": "q"}], True, None):
pass