diff --git a/.env.example b/.env.example index 10276350..542e0859 100644 --- a/.env.example +++ b/.env.example @@ -8,10 +8,6 @@ LLM_MODEL_NAME=qwen-plus # ===== ZEP记忆图谱配置 ===== # 每月免费额度即可支撑简单使用:https://app.getzep.com/ ZEP_API_KEY=your_zep_api_key_here -# 单次 Zep Cloud 请求超时(秒) -ZEP_REQUEST_TIMEOUT_SECONDS=30 -# 文档批次或模拟 episode 等待处理完成的最长时间(秒) -ZEP_INGESTION_TIMEOUT_SECONDS=600 # ===== 加速 LLM 配置(可选)===== # 注意如果不使用加速配置,env文件中就不要出现下面的配置项 diff --git a/backend/app/config.py b/backend/app/config.py index 534e3baf..02aed9a7 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -14,29 +14,6 @@ else: load_dotenv(override=True) -def _parse_number(name, raw_value, *, default, cast): - """Parse a numeric setting without making module import fail.""" - - try: - return cast(raw_value), None - except (TypeError, ValueError): - return default, f"{name} must be a number" - - -_zep_request_timeout, _zep_request_timeout_error = _parse_number( - "ZEP_REQUEST_TIMEOUT_SECONDS", - os.environ.get("ZEP_REQUEST_TIMEOUT_SECONDS", "30"), - default=30.0, - cast=float, -) -_zep_ingestion_timeout, _zep_ingestion_timeout_error = _parse_number( - "ZEP_INGESTION_TIMEOUT_SECONDS", - os.environ.get("ZEP_INGESTION_TIMEOUT_SECONDS", "600"), - default=600, - cast=int, -) - - class Config: """Flask配置类""" @@ -54,13 +31,6 @@ class Config: # Zep配置 ZEP_API_KEY = os.environ.get('ZEP_API_KEY') - ZEP_REQUEST_TIMEOUT_SECONDS = _zep_request_timeout - ZEP_INGESTION_TIMEOUT_SECONDS = _zep_ingestion_timeout - _ZEP_CONFIG_PARSE_ERRORS = tuple( - error - for error in (_zep_request_timeout_error, _zep_ingestion_timeout_error) - if error - ) # 文件上传配置 MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB @@ -100,11 +70,6 @@ class Config: errors.append("ZEP_API_KEY 未配置") if os.environ.get("ZEP_API_URL"): errors.append("ZEP_API_URL 不受支持;MiroFish 仅连接 Zep Cloud") - errors.extend(cls._ZEP_CONFIG_PARSE_ERRORS) - if cls.ZEP_REQUEST_TIMEOUT_SECONDS <= 0: - errors.append("ZEP_REQUEST_TIMEOUT_SECONDS 必须大于 0") - if cls.ZEP_INGESTION_TIMEOUT_SECONDS <= 0: - errors.append("ZEP_INGESTION_TIMEOUT_SECONDS 必须大于 0") if cls.DEBUG: import warnings warnings.warn("Flask DEBUG mode is enabled. Do not use in production.", RuntimeWarning) diff --git a/backend/app/services/graph_builder.py b/backend/app/services/graph_builder.py index 33706914..0abe8d18 100644 --- a/backend/app/services/graph_builder.py +++ b/backend/app/services/graph_builder.py @@ -22,6 +22,7 @@ from ..utils.ontology import ( normalize_ontology_source_targets, ) from ..utils.zep import ( + ZEP_INGESTION_WAIT_TIMEOUT_SECONDS, call_zep_read_with_retry, get_zep_client, is_retryable_zep_error, @@ -635,7 +636,7 @@ class GraphBuilderService: ) -> List[str]: """Wait for a Batch API terminal state and validate every item.""" - timeout = timeout or Config.ZEP_INGESTION_TIMEOUT_SECONDS + timeout = timeout or ZEP_INGESTION_WAIT_TIMEOUT_SECONDS start_time = time.time() terminal_states = {"succeeded", "partial", "failed", "invalid", "canceled"} @@ -720,7 +721,7 @@ class GraphBuilderService: self, episode_uuids: List[str], progress_callback: Optional[Callable] = None, - timeout: int = 600 + timeout: int = ZEP_INGESTION_WAIT_TIMEOUT_SECONDS ): """等待所有 episode 处理完成(通过查询每个 episode 的 processed 状态)""" if not episode_uuids: diff --git a/backend/app/services/simulation_runner.py b/backend/app/services/simulation_runner.py index b3bdb39b..b1b99897 100644 --- a/backend/app/services/simulation_runner.py +++ b/backend/app/services/simulation_runner.py @@ -21,6 +21,10 @@ from queue import Queue from ..config import Config from ..utils.logger import get_logger from ..utils.locale import get_locale, set_locale +from ..utils.zep import ( + ZEP_HTTP_REQUEST_TIMEOUT_SECONDS, + ZEP_INGESTION_WAIT_TIMEOUT_SECONDS, +) from .zep_graph_memory_updater import ZepGraphMemoryManager from .simulation_ipc import SimulationIPCClient, CommandType, IPCResponse @@ -1021,8 +1025,8 @@ class SimulationRunner: ): wait_timeout = max( 30.0, - Config.ZEP_INGESTION_TIMEOUT_SECONDS - + Config.ZEP_REQUEST_TIMEOUT_SECONDS + ZEP_INGESTION_WAIT_TIMEOUT_SECONDS + + ZEP_HTTP_REQUEST_TIMEOUT_SECONDS + 5, ) monitor.join(timeout=wait_timeout) diff --git a/backend/app/services/zep_graph_memory_updater.py b/backend/app/services/zep_graph_memory_updater.py index 343ca1f2..2f50050d 100644 --- a/backend/app/services/zep_graph_memory_updater.py +++ b/backend/app/services/zep_graph_memory_updater.py @@ -13,7 +13,11 @@ from queue import Queue, Empty from ..config import Config from ..utils.logger import get_logger from ..utils.locale import get_locale, set_locale -from ..utils.zep import call_zep_read_with_retry, get_zep_client +from ..utils.zep import ( + ZEP_INGESTION_WAIT_TIMEOUT_SECONDS, + call_zep_read_with_retry, + get_zep_client, +) logger = get_logger('mirofish.zep_graph_memory_updater') @@ -308,7 +312,7 @@ class ZepGraphMemoryUpdater: def stop(self): """Drain the worker, flush tail events, and wait for Cloud ingestion.""" - deadline = time.time() + Config.ZEP_INGESTION_TIMEOUT_SECONDS + deadline = time.time() + ZEP_INGESTION_WAIT_TIMEOUT_SECONDS # Serialize the accepting->closed transition with add_activity's # check+enqueue operation. This closes the small race where a producer # could enqueue after both the worker and final flush had exited. @@ -598,7 +602,7 @@ class ZepGraphMemoryUpdater: return if deadline is None: - deadline = time.time() + Config.ZEP_INGESTION_TIMEOUT_SECONDS + deadline = time.time() + ZEP_INGESTION_WAIT_TIMEOUT_SECONDS while pending: if time.time() >= deadline: raise TimeoutError( diff --git a/backend/app/utils/zep.py b/backend/app/utils/zep.py index 16dbe20e..c8bd4c96 100644 --- a/backend/app/utils/zep.py +++ b/backend/app/utils/zep.py @@ -19,6 +19,13 @@ logger = get_logger("mirofish.zep") T = TypeVar("T") ZEP_CLOUD_BASE_URL = "https://api.getzep.com/api/v2" +# Keep request behavior aligned with the zep-cloud 3.25.0 SDK default that +# MiroFish used before introducing the shared client. This is an internal +# integration policy, not a deployment setting users need to tune. +ZEP_HTTP_REQUEST_TIMEOUT_SECONDS = 60.0 +# Zep ingestion is asynchronous and may take several minutes. Preserve the +# original GraphBuilder deadline while keeping it separate from HTTP timeout. +ZEP_INGESTION_WAIT_TIMEOUT_SECONDS = 600 MAX_ZEP_SEARCH_QUERY_CHARS = 400 MAX_ZEP_SEARCH_RESULTS = 50 @@ -58,9 +65,6 @@ def _cached_zep_client(api_key: str, timeout: float) -> Zep: def get_zep_client(api_key: str | None = None, timeout: float | None = None) -> Zep: """Return a process-shared, explicitly configured Zep Cloud client.""" - if Config._ZEP_CONFIG_PARSE_ERRORS: - raise ValueError("; ".join(Config._ZEP_CONFIG_PARSE_ERRORS)) - # zep-cloud gives ZEP_API_URL precedence even when base_url is explicit. # Reject it so this Cloud-only integration cannot silently target a # self-hosted or compatibility endpoint. @@ -72,7 +76,7 @@ def get_zep_client(api_key: str | None = None, timeout: float | None = None) -> raise ValueError("ZEP_API_KEY 未配置") request_timeout = float( - timeout if timeout is not None else Config.ZEP_REQUEST_TIMEOUT_SECONDS + timeout if timeout is not None else ZEP_HTTP_REQUEST_TIMEOUT_SECONDS ) if request_timeout <= 0: raise ValueError("Zep request timeout must be greater than 0") diff --git a/backend/tests/test_zep_graph_memory_updater.py b/backend/tests/test_zep_graph_memory_updater.py index 5f5531b0..aec703e9 100644 --- a/backend/tests/test_zep_graph_memory_updater.py +++ b/backend/tests/test_zep_graph_memory_updater.py @@ -187,7 +187,7 @@ def test_pending_episode_wait_has_a_deadline(monkeypatch): processed=False ) timestamps = iter([0.0, 2.0]) - monkeypatch.setattr(updater_module.Config, "ZEP_INGESTION_TIMEOUT_SECONDS", 1) + monkeypatch.setattr(updater_module, "ZEP_INGESTION_WAIT_TIMEOUT_SECONDS", 1) monkeypatch.setattr(updater_module.time, "time", lambda: next(timestamps)) monkeypatch.setattr(updater_module.time, "sleep", lambda _seconds: None) diff --git a/backend/tests/test_zep_retry_and_client.py b/backend/tests/test_zep_retry_and_client.py index 5e8d4494..eac4fe9b 100644 --- a/backend/tests/test_zep_retry_and_client.py +++ b/backend/tests/test_zep_retry_and_client.py @@ -1,10 +1,10 @@ +from pathlib import Path from types import SimpleNamespace import pytest from zep_cloud.core.api_error import ApiError as ZepApiError from app.utils import zep -from app import config def test_permanent_zep_errors_fail_without_retry(): @@ -79,24 +79,34 @@ def test_zep_client_rejects_self_hosted_endpoint_override(monkeypatch): zep.get_zep_client("test-key") -def test_malformed_timeout_config_is_reported_without_import_failure(): - value, error = config._parse_number( - "ZEP_REQUEST_TIMEOUT_SECONDS", - "not-a-number", - default=30.0, - cast=float, - ) +def test_zep_client_uses_internal_timeout_and_ignores_env_overrides(monkeypatch): + created = [] - assert value == 30.0 - assert "must be a number" in error + def fake_zep(**kwargs): + created.append(kwargs) + return SimpleNamespace(kwargs=kwargs) + + monkeypatch.delenv("ZEP_API_URL", raising=False) + monkeypatch.setenv("ZEP_REQUEST_TIMEOUT_SECONDS", "1") + monkeypatch.setenv("ZEP_INGESTION_TIMEOUT_SECONDS", "1") + monkeypatch.setattr(zep, "Zep", fake_zep) + zep.clear_zep_client_cache() + + zep.get_zep_client("test-key") + + assert created == [{ + "api_key": "test-key", + "base_url": zep.ZEP_CLOUD_BASE_URL, + "timeout": zep.ZEP_HTTP_REQUEST_TIMEOUT_SECONDS, + }] + assert zep.ZEP_HTTP_REQUEST_TIMEOUT_SECONDS == 60.0 + assert zep.ZEP_INGESTION_WAIT_TIMEOUT_SECONDS == 600 + zep.clear_zep_client_cache() -def test_zep_client_rejects_a_recorded_timeout_parse_error(monkeypatch): - monkeypatch.setattr( - zep.Config, - "_ZEP_CONFIG_PARSE_ERRORS", - ("ZEP_REQUEST_TIMEOUT_SECONDS must be a number",), - ) +def test_zep_timeout_policy_is_not_exposed_in_env_example(): + env_example = Path(__file__).resolve().parents[2] / ".env.example" + contents = env_example.read_text(encoding="utf-8") - with pytest.raises(ValueError, match="must be a number"): - zep.get_zep_client("test-key") + assert "ZEP_REQUEST_TIMEOUT_SECONDS" not in contents + assert "ZEP_INGESTION_TIMEOUT_SECONDS" not in contents