mirror of
https://github.com/sligter/LandPPT.git
synced 2026-08-30 17:34:51 +08:00
152876610a
Add optional TAVILY_BASE_URL environment variable and configuration option to enable custom Tavily API endpoints, such as proxies or private gateways. Updated core configuration, research service initialization, UI templates, and tests to handle the new base URL parameter. Also enhanced outline streaming with force_regenerate option for improved regeneration control.
363 lines
14 KiB
Python
363 lines
14 KiB
Python
import asyncio
|
|
import json
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
|
|
def test_config_service_defaults_enable_per_slide_creative_guidance():
|
|
from landppt.services.config_service import ConfigService
|
|
|
|
service = ConfigService(env_file=".env.example")
|
|
|
|
assert service.config_schema["enable_per_slide_creative_guidance"]["default"] == "true"
|
|
|
|
|
|
def test_db_config_service_defaults_enable_per_slide_creative_guidance():
|
|
from landppt.services.db_config_service import DatabaseConfigService
|
|
|
|
service = DatabaseConfigService()
|
|
|
|
assert service.config_schema["enable_per_slide_creative_guidance"]["default"] == "true"
|
|
|
|
|
|
def test_tavily_base_url_exists_in_config_schemas():
|
|
from landppt.services.config_service import ConfigService
|
|
from landppt.services.db_config_service import DatabaseConfigService
|
|
|
|
env_service = ConfigService(env_file=".env.example")
|
|
db_service = DatabaseConfigService()
|
|
|
|
assert env_service.config_schema["tavily_base_url"]["type"] == "url"
|
|
assert env_service.config_schema["tavily_base_url"]["category"] == "generation_params"
|
|
assert env_service.config_schema["tavily_base_url"]["default"] == "https://api.tavily.com"
|
|
assert db_service.config_schema["tavily_base_url"]["type"] == "url"
|
|
assert db_service.config_schema["tavily_base_url"]["category"] == "generation_params"
|
|
assert db_service.config_schema["tavily_base_url"]["default"] == "https://api.tavily.com"
|
|
|
|
|
|
def test_db_config_service_resolves_blank_tavily_base_url_to_official_default():
|
|
from landppt.services.db_config_service import DatabaseConfigService
|
|
|
|
service = DatabaseConfigService()
|
|
resolved = service._resolve_config_values(
|
|
{
|
|
"tavily_base_url": {
|
|
"value": "",
|
|
"type": "url",
|
|
"category": "generation_params",
|
|
"is_user_override": True,
|
|
},
|
|
},
|
|
{},
|
|
)
|
|
|
|
assert resolved["tavily_base_url"] == "https://api.tavily.com"
|
|
|
|
|
|
def test_apryse_standard_pptx_export_is_admin_only_in_config_schema():
|
|
from landppt.services.config_service import ConfigService
|
|
from landppt.services.db_config_service import DatabaseConfigService
|
|
|
|
env_service = ConfigService(env_file=".env.example")
|
|
db_service = DatabaseConfigService()
|
|
|
|
assert env_service.config_schema["enable_apryse_pptx_export"]["default"] == "false"
|
|
assert env_service.config_schema["enable_apryse_pptx_export"]["admin_only"] is True
|
|
assert env_service.config_schema["apryse_license_key"]["admin_only"] is True
|
|
assert db_service.config_schema["enable_apryse_pptx_export"]["default"] == "false"
|
|
assert db_service.config_schema["enable_apryse_pptx_export"]["admin_only"] is True
|
|
assert db_service.config_schema["apryse_license_key"]["admin_only"] is True
|
|
|
|
|
|
def test_db_config_service_filters_and_resolves_admin_only_apryse_config(monkeypatch):
|
|
from landppt.services.db_config_service import DatabaseConfigService
|
|
|
|
service = DatabaseConfigService()
|
|
public_schema = service.get_config_schema(include_admin_only=False)
|
|
resolved = service._resolve_config_values(
|
|
{
|
|
"apryse_license_key": {"value": "user-license", "type": "password", "category": "generation_params"},
|
|
"enable_apryse_pptx_export": {"value": "false", "type": "boolean", "category": "generation_params"},
|
|
},
|
|
{
|
|
"apryse_license_key": {"value": "system-license", "type": "password", "category": "generation_params"},
|
|
"enable_apryse_pptx_export": {"value": "true", "type": "boolean", "category": "generation_params"},
|
|
},
|
|
)
|
|
|
|
async def fake_get_all_config(user_id=None):
|
|
return {
|
|
"apryse_license_key": "system-license",
|
|
"enable_apryse_pptx_export": True,
|
|
"openai_model": "gpt-4.1",
|
|
}
|
|
|
|
monkeypatch.setattr(service, "get_all_config", fake_get_all_config)
|
|
filtered = asyncio.run(service.get_all_config_for_user(user_id=1, is_admin=False))
|
|
|
|
assert "apryse_license_key" not in public_schema
|
|
assert "enable_apryse_pptx_export" not in public_schema
|
|
assert resolved["apryse_license_key"] == "system-license"
|
|
assert resolved["enable_apryse_pptx_export"] is True
|
|
assert "apryse_license_key" not in filtered
|
|
assert "enable_apryse_pptx_export" not in filtered
|
|
assert filtered["openai_model"] == "gpt-4.1"
|
|
|
|
|
|
def test_db_config_service_resolves_system_scope_admin_only_config_from_merged_rows():
|
|
from landppt.services.db_config_service import DatabaseConfigService
|
|
|
|
service = DatabaseConfigService()
|
|
resolved = service._resolve_config_values(
|
|
{
|
|
"apryse_license_key": {
|
|
"value": "system-license",
|
|
"type": "password",
|
|
"category": "generation_params",
|
|
"is_user_override": False,
|
|
},
|
|
"enable_apryse_pptx_export": {
|
|
"value": "true",
|
|
"type": "boolean",
|
|
"category": "generation_params",
|
|
"is_user_override": False,
|
|
},
|
|
},
|
|
{},
|
|
)
|
|
|
|
assert resolved["apryse_license_key"] == "system-license"
|
|
assert resolved["enable_apryse_pptx_export"] is True
|
|
|
|
|
|
def test_db_config_service_ignores_user_override_for_admin_only_when_no_system_default():
|
|
from landppt.services.db_config_service import DatabaseConfigService
|
|
|
|
service = DatabaseConfigService()
|
|
resolved = service._resolve_config_values(
|
|
{
|
|
"apryse_license_key": {
|
|
"value": "legacy-user-license",
|
|
"type": "password",
|
|
"category": "generation_params",
|
|
"is_user_override": True,
|
|
},
|
|
"enable_apryse_pptx_export": {
|
|
"value": "true",
|
|
"type": "boolean",
|
|
"category": "generation_params",
|
|
"is_user_override": True,
|
|
},
|
|
},
|
|
{},
|
|
)
|
|
|
|
assert resolved["apryse_license_key"] in (None, "")
|
|
assert resolved["enable_apryse_pptx_export"] is False
|
|
|
|
|
|
def test_db_config_service_migrates_legacy_system_default_to_true(monkeypatch):
|
|
import landppt.database.database as database_mod
|
|
import landppt.database.repositories as repo_mod
|
|
from landppt.services.db_config_service import DatabaseConfigService
|
|
|
|
captured = {"set_calls": []}
|
|
|
|
class FakeSession:
|
|
async def __aenter__(self):
|
|
return self
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
async def commit(self):
|
|
return None
|
|
|
|
class FakeRepo:
|
|
def __init__(self, session):
|
|
self.session = session
|
|
|
|
async def get_all_configs(self, user_id=None):
|
|
return {
|
|
"enable_per_slide_creative_guidance": {"value": "false"},
|
|
}
|
|
|
|
async def set_config(self, **kwargs):
|
|
captured["set_calls"].append(kwargs)
|
|
|
|
monkeypatch.setattr(database_mod, "AsyncSessionLocal", lambda: FakeSession())
|
|
monkeypatch.setattr(repo_mod, "UserConfigRepository", FakeRepo)
|
|
|
|
service = DatabaseConfigService()
|
|
updated = asyncio.run(service.initialize_system_defaults())
|
|
|
|
assert updated >= 1
|
|
assert any(
|
|
call["key"] == "enable_per_slide_creative_guidance" and call["value"] == "true"
|
|
for call in captured["set_calls"]
|
|
)
|
|
|
|
|
|
def test_creative_design_service_uses_slide_prompt_when_per_slide_guidance_enabled(tmp_path):
|
|
from landppt.services.slide.creative_design_service import CreativeDesignService
|
|
|
|
class _DummyService:
|
|
def __init__(self):
|
|
self.user_id = 1
|
|
self.cache_dirs = {"style_genes": tmp_path}
|
|
self._cached_style_genes = {}
|
|
self._cached_global_constitutions = {}
|
|
self._cached_page_creative_briefs = {}
|
|
self._cached_slide_creative_guides = {}
|
|
self._style_genes_ready_events = {}
|
|
self._global_constitution_ready_events = {}
|
|
self._page_creative_brief_ready_events = {}
|
|
self._slide_creative_guide_ready_events = {}
|
|
|
|
async def _get_user_generation_config(self):
|
|
return {"enable_per_slide_creative_guidance": True}
|
|
|
|
owner = _DummyService()
|
|
service = CreativeDesignService(owner)
|
|
|
|
async def fake_style_genes(project_id, template_html, page_number):
|
|
return "STYLE-GENES"
|
|
|
|
async def fake_constitution(*args, **kwargs):
|
|
return "GLOBAL-CONSTITUTION"
|
|
|
|
async def fail_page_briefs(*args, **kwargs):
|
|
raise AssertionError("page-type briefs should not be used when per-slide guidance is enabled")
|
|
|
|
async def fake_slide_guide(*args, **kwargs):
|
|
return "SLIDE-GUIDE"
|
|
|
|
service._get_or_extract_style_genes = fake_style_genes
|
|
service._get_or_generate_global_constitution = fake_constitution
|
|
service._get_or_generate_page_creative_briefs = fail_page_briefs
|
|
service._get_or_generate_slide_creative_guide = fake_slide_guide
|
|
|
|
result = asyncio.run(
|
|
service._get_creative_design_inputs(
|
|
project_id="proj-1",
|
|
template_html="<div></div>",
|
|
slide_data={"title": "增长机会"},
|
|
page_number=2,
|
|
total_pages=5,
|
|
confirmed_requirements={"topic": "年度复盘"},
|
|
all_slides=[{"title": "封面"}, {"title": "增长机会"}],
|
|
)
|
|
)
|
|
|
|
assert result == ("STYLE-GENES", "GLOBAL-CONSTITUTION", "SLIDE-GUIDE")
|
|
|
|
|
|
def test_creative_design_service_uses_page_type_briefs_when_per_slide_guidance_disabled(tmp_path):
|
|
from landppt.services.slide.creative_design_service import CreativeDesignService
|
|
|
|
class _DummyService:
|
|
def __init__(self):
|
|
self.user_id = 1
|
|
self.cache_dirs = {"style_genes": tmp_path}
|
|
self._cached_style_genes = {}
|
|
self._cached_global_constitutions = {}
|
|
self._cached_page_creative_briefs = {}
|
|
self._cached_slide_creative_guides = {}
|
|
self._style_genes_ready_events = {}
|
|
self._global_constitution_ready_events = {}
|
|
self._page_creative_brief_ready_events = {}
|
|
self._slide_creative_guide_ready_events = {}
|
|
|
|
async def _get_user_generation_config(self):
|
|
return {"enable_per_slide_creative_guidance": False}
|
|
|
|
owner = _DummyService()
|
|
service = CreativeDesignService(owner)
|
|
|
|
async def fake_style_genes(project_id, template_html, page_number):
|
|
return "STYLE-GENES"
|
|
|
|
async def fake_constitution(*args, **kwargs):
|
|
return "GLOBAL-CONSTITUTION"
|
|
|
|
async def fake_page_briefs(*args, **kwargs):
|
|
return [
|
|
{"page": 1, "creative_brief": "封面指导"},
|
|
{"page": 2, "creative_brief": "PAGE-BRIEF"},
|
|
]
|
|
|
|
async def fail_slide_guide(*args, **kwargs):
|
|
raise AssertionError("slide-level prompt should not be used when per-slide guidance is disabled")
|
|
|
|
service._get_or_extract_style_genes = fake_style_genes
|
|
service._get_or_generate_global_constitution = fake_constitution
|
|
service._get_or_generate_page_creative_briefs = fake_page_briefs
|
|
service._get_or_generate_slide_creative_guide = fail_slide_guide
|
|
|
|
result = asyncio.run(
|
|
service._get_creative_design_inputs(
|
|
project_id="proj-1",
|
|
template_html="<div></div>",
|
|
slide_data={"title": "增长机会"},
|
|
page_number=2,
|
|
total_pages=5,
|
|
confirmed_requirements={"topic": "年度复盘"},
|
|
all_slides=[{"title": "封面"}, {"title": "增长机会"}],
|
|
)
|
|
)
|
|
|
|
assert result == ("STYLE-GENES", "GLOBAL-CONSTITUTION", "PAGE-BRIEF")
|
|
|
|
|
|
def test_creative_design_service_persists_slide_creative_guide_cache(tmp_path, monkeypatch):
|
|
from landppt.services.slide.creative_design_service import CreativeDesignService
|
|
|
|
class _DummyService:
|
|
def __init__(self):
|
|
self.user_id = 1
|
|
self.cache_dirs = {"style_genes": tmp_path}
|
|
self._cached_slide_creative_guides = {}
|
|
self._slide_creative_guide_ready_events = {}
|
|
|
|
owner = _DummyService()
|
|
service = CreativeDesignService(owner)
|
|
recorded = []
|
|
|
|
async def fake_generate(*args, **kwargs):
|
|
recorded.append((args, kwargs))
|
|
return "REALTIME-GUIDE"
|
|
|
|
monkeypatch.setattr(service, "_generate_slide_creative_guide", fake_generate)
|
|
|
|
guide = asyncio.run(
|
|
service._get_or_generate_slide_creative_guide(
|
|
project_id="proj-cache",
|
|
slide_data={"title": "当前页"},
|
|
page_number=3,
|
|
total_pages=8,
|
|
confirmed_requirements={"topic": "项目汇报"},
|
|
all_slides=[{"title": "封面"}, {"title": "目录"}, {"title": "当前页"}],
|
|
template_html="<div class='page'></div>",
|
|
)
|
|
)
|
|
|
|
cache_file = Path(tmp_path) / "proj-cache_slide_3_creative_guide.json"
|
|
cache_data = json.loads(cache_file.read_text(encoding="utf-8"))
|
|
|
|
assert guide == "REALTIME-GUIDE"
|
|
assert len(recorded) == 1
|
|
assert owner._cached_slide_creative_guides["proj-cache:3"] == "REALTIME-GUIDE"
|
|
assert cache_data["creative_guide"] == "REALTIME-GUIDE"
|
|
|
|
|
|
def test_creative_guidance_runtime_pipeline_wires_per_slide_prompt_and_cache_cleanup():
|
|
root = Path(__file__).resolve().parents[1]
|
|
service_text = (root / "src/landppt/services/slide/creative_design_service.py").read_text(encoding="utf-8")
|
|
|
|
assert 'enable_per_slide_creative_guidance' in service_text
|
|
assert 'get_slide_design_guide_prompt(' in service_text
|
|
assert '_get_or_generate_slide_creative_guide(' in service_text
|
|
assert '_cached_slide_creative_guides' in service_text
|
|
assert '_slide_creative_guide_ready_events' in service_text
|
|
assert '_slide_*_creative_guide.json' in service_text
|