Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 74 additions & 5 deletions backend/app/services/zep_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@

import time
import json
import re
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field

from zep_cloud.client import Zep
from zep_cloud.core.api_error import ApiError

from ..config import Config
from ..utils.logger import get_logger
Expand All @@ -23,6 +25,55 @@

logger = get_logger('mirofish.zep_tools')

_MAX_RATE_LIMIT_DELAY = 60.0


def _is_rate_limit_error(error: Exception) -> bool:
status_code = getattr(error, "status_code", None)
if status_code == 429:
return True

body = getattr(error, "body", None)
if isinstance(body, str) and "rate limit" in body.lower():
return True

text = str(error).lower()
return "status_code: 429" in text or "rate limit" in text or "too many requests" in text


def _parse_retry_after(error: Exception) -> float | None:
headers = getattr(error, "headers", None)
if isinstance(headers, dict):
retry_after = headers.get("retry-after") or headers.get("Retry-After")
if retry_after:
try:
return max(float(retry_after), 0.0)
except (TypeError, ValueError):
pass

reset = headers.get("x-ratelimit-reset") or headers.get("X-RateLimit-Reset")
if reset:
try:
reset_seconds = float(reset)
if reset_seconds > 1_000_000_000:
wait_seconds = reset_seconds - time.time()
else:
wait_seconds = reset_seconds
if wait_seconds > 0:
return wait_seconds
except (TypeError, ValueError):
pass

text = str(error)
match = re.search(r"retry-after['\"]?\s*:\s*['\"]?(\d+(?:\.\d+)?)", text, re.IGNORECASE)
if match:
try:
return max(float(match.group(1)), 0.0)
except (TypeError, ValueError):
pass

return None


@dataclass
class SearchResult:
Expand Down Expand Up @@ -448,14 +499,32 @@ def _call_with_retry(self, func, operation_name: str, max_retries: int = None):
for attempt in range(max_retries):
try:
return func()
except Exception as e:
except (Exception, ApiError) as e:
last_exception = e
if attempt < max_retries - 1:
logger.warning(
t("console.zepRetryAttempt", operation=operation_name, attempt=attempt + 1, error=str(e)[:100], delay=f"{delay:.1f}")
)
if _is_rate_limit_error(e):
retry_after = _parse_retry_after(e)
if retry_after is not None:
delay = min(max(retry_after, self.RETRY_DELAY), _MAX_RATE_LIMIT_DELAY)
else:
delay = min(max(delay, self.RETRY_DELAY), _MAX_RATE_LIMIT_DELAY)
else:
delay = min(delay, _MAX_RATE_LIMIT_DELAY)

if _is_rate_limit_error(e):
logger.warning(
f"Zep {operation_name} rate limit hit (attempt {attempt + 1}/{max_retries}); "
f"retrying in {delay:.1f}s..."
)
else:
logger.warning(
t("console.zepRetryAttempt", operation=operation_name, attempt=attempt + 1, error=str(e)[:100], delay=f"{delay:.1f}")
)
time.sleep(delay)
delay *= 2
if _is_rate_limit_error(e):
delay = min(delay * 1.25, _MAX_RATE_LIMIT_DELAY)
else:
delay = min(delay * 2, _MAX_RATE_LIMIT_DELAY)
else:
logger.error(t("console.zepAllRetriesFailed", operation=operation_name, retries=max_retries, error=str(e)))

Expand Down
82 changes: 76 additions & 6 deletions backend/app/utils/zep_paging.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
from __future__ import annotations

import time
import re
from collections.abc import Callable
from typing import Any

from zep_cloud import InternalServerError
from zep_cloud.core.api_error import ApiError
from zep_cloud.client import Zep

from .logger import get_logger
Expand All @@ -19,8 +21,57 @@

_DEFAULT_PAGE_SIZE = 100
_MAX_NODES = 2000
_DEFAULT_MAX_RETRIES = 3
_DEFAULT_MAX_RETRIES = 5
_DEFAULT_RETRY_DELAY = 2.0 # seconds, doubles each retry
_MAX_RETRY_DELAY = 60.0


def _is_rate_limit_error(error: Exception) -> bool:
status_code = getattr(error, "status_code", None)
if status_code == 429:
return True

body = getattr(error, "body", None)
if isinstance(body, str) and "rate limit" in body.lower():
return True

text = str(error).lower()
return "status_code: 429" in text or "rate limit" in text or "too many requests" in text


def _parse_retry_after(error: Exception) -> float | None:
headers = getattr(error, "headers", None)
if isinstance(headers, dict):
retry_after = headers.get("retry-after") or headers.get("Retry-After")
if retry_after:
try:
return max(float(retry_after), 0.0)
except (TypeError, ValueError):
pass

reset = headers.get("x-ratelimit-reset") or headers.get("X-RateLimit-Reset")
if reset:
try:
reset_seconds = float(reset)
if reset_seconds > 1_000_000_000:
# Some providers return a unix timestamp instead of a delta.
wait_seconds = reset_seconds - time.time()
else:
wait_seconds = reset_seconds
if wait_seconds > 0:
return wait_seconds
except (TypeError, ValueError):
pass

text = str(error)
match = re.search(r"retry-after['\"]?\s*:\s*['\"]?(\d+(?:\.\d+)?)", text, re.IGNORECASE)
if match:
try:
return max(float(match.group(1)), 0.0)
except (TypeError, ValueError):
pass

return None


def _fetch_page_with_retry(
Expand All @@ -41,14 +92,33 @@ def _fetch_page_with_retry(
for attempt in range(max_retries):
try:
return api_call(*args, **kwargs)
except (ConnectionError, TimeoutError, OSError, InternalServerError) as e:
except (ConnectionError, TimeoutError, OSError, InternalServerError, ApiError) as e:
last_exception = e
if attempt < max_retries - 1:
logger.warning(
f"Zep {page_description} attempt {attempt + 1} failed: {str(e)[:100]}, retrying in {delay:.1f}s..."
)
if _is_rate_limit_error(e):
retry_after = _parse_retry_after(e)
if retry_after is not None:
delay = min(max(retry_after, retry_delay), _MAX_RETRY_DELAY)
else:
delay = min(max(delay, retry_delay), _MAX_RETRY_DELAY)
else:
delay = min(delay, _MAX_RETRY_DELAY)

if _is_rate_limit_error(e):
logger.warning(
f"Zep {page_description} rate limit hit (attempt {attempt + 1}/{max_retries}); "
f"retrying in {delay:.1f}s..."
)
else:
logger.warning(
f"Zep {page_description} attempt {attempt + 1} failed: {str(e)[:100]}, retrying in {delay:.1f}s..."
)
time.sleep(delay)
delay *= 2
if _is_rate_limit_error(e):
# Respect server-advised retry delays; keep the same delay or back off slightly.
delay = min(delay * 1.25, _MAX_RETRY_DELAY)
else:
delay = min(delay * 2, _MAX_RETRY_DELAY)
else:
logger.error(f"Zep {page_description} failed after {max_retries} attempts: {str(e)}")

Expand Down
15 changes: 6 additions & 9 deletions frontend/src/api/graph.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,9 @@ import service, { requestWithRetry } from './index'
export function generateOntology(formData) {
return requestWithRetry(() =>
service({
url: '/api/graph/ontology/generate',
url: '/graph/ontology/generate',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
data: formData
})
)
}
Expand All @@ -26,7 +23,7 @@ export function generateOntology(formData) {
export function buildGraph(data) {
return requestWithRetry(() =>
service({
url: '/api/graph/build',
url: '/graph/build',
method: 'post',
data
})
Expand All @@ -40,7 +37,7 @@ export function buildGraph(data) {
*/
export function getTaskStatus(taskId) {
return service({
url: `/api/graph/task/${taskId}`,
url: `/graph/task/${taskId}`,
method: 'get'
})
}
Expand All @@ -52,7 +49,7 @@ export function getTaskStatus(taskId) {
*/
export function getGraphData(graphId) {
return service({
url: `/api/graph/data/${graphId}`,
url: `/graph/data/${graphId}`,
method: 'get'
})
}
Expand All @@ -64,7 +61,7 @@ export function getGraphData(graphId) {
*/
export function getProject(projectId) {
return service({
url: `/api/graph/project/${projectId}`,
url: `/graph/project/${projectId}`,
method: 'get'
})
}
12 changes: 7 additions & 5 deletions frontend/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import i18n from '../i18n'

// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5001',
timeout: 300000, // 5分钟超时(本体生成可能需要较长时间)
headers: {
'Content-Type': 'application/json'
}
// Default to the Vite proxy so the frontend works both on localhost
// and when accessed from another machine via the dev server host.
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 300000 // 5分钟超时(本体生成可能需要较长时间)
})

// 请求拦截器
service.interceptors.request.use(
config => {
config.headers['Accept-Language'] = i18n.global.locale.value
if (config.data instanceof FormData) {
delete config.headers['Content-Type']
}
return config
},
error => {
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/api/report.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ import service, { requestWithRetry } from './index'
* @param {Object} data - { simulation_id, force_regenerate? }
*/
export const generateReport = (data) => {
return requestWithRetry(() => service.post('/api/report/generate', data), 3, 1000)
return requestWithRetry(() => service.post('/report/generate', data), 3, 1000)
}

/**
* 获取报告生成状态
* @param {string} reportId
*/
export const getReportStatus = (reportId) => {
return service.get(`/api/report/generate/status`, { params: { report_id: reportId } })
return service.get(`/report/generate/status`, { params: { report_id: reportId } })
}

/**
Expand All @@ -22,7 +22,7 @@ export const getReportStatus = (reportId) => {
* @param {number} fromLine - 从第几行开始获取
*/
export const getAgentLog = (reportId, fromLine = 0) => {
return service.get(`/api/report/${reportId}/agent-log`, { params: { from_line: fromLine } })
return service.get(`/report/${reportId}/agent-log`, { params: { from_line: fromLine } })
}

/**
Expand All @@ -31,21 +31,21 @@ export const getAgentLog = (reportId, fromLine = 0) => {
* @param {number} fromLine - 从第几行开始获取
*/
export const getConsoleLog = (reportId, fromLine = 0) => {
return service.get(`/api/report/${reportId}/console-log`, { params: { from_line: fromLine } })
return service.get(`/report/${reportId}/console-log`, { params: { from_line: fromLine } })
}

/**
* 获取报告详情
* @param {string} reportId
*/
export const getReport = (reportId) => {
return service.get(`/api/report/${reportId}`)
return service.get(`/report/${reportId}`)
}

/**
* 与 Report Agent 对话
* @param {Object} data - { simulation_id, message, chat_history? }
*/
export const chatWithReport = (data) => {
return requestWithRetry(() => service.post('/api/report/chat', data), 3, 1000)
return requestWithRetry(() => service.post('/report/chat', data), 3, 1000)
}
Loading