fix: keep Zep timeouts internal

This commit is contained in:
666ghj 2026-07-23 00:32:53 +08:00
parent c81d628975
commit 407c391f35
8 changed files with 53 additions and 69 deletions

View File

@ -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文件中就不要出现下面的配置项

View File

@ -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)

View File

@ -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:

View File

@ -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)

View File

@ -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(

View File

@ -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")

View File

@ -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)

View File

@ -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