mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 22:14:46 +00:00
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:
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
|
||||
|
||||
|
||||
Reference in new issue
Block a user