This commit is contained in:
PhoenixCPH 2026-05-25 00:07:45 +02:00 committed by GitHub
commit 6f209ecb3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 310 additions and 54 deletions

View File

@ -10,10 +10,12 @@ Zep检索工具服务
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
@ -23,6 +25,55 @@ from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
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:
@ -448,14 +499,32 @@ class ZepToolsService:
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)))

View File

@ -7,10 +7,12 @@ Zep 的 node/edge 列表接口使用 UUID cursor 分页,
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
@ -19,8 +21,57 @@ logger = get_logger('mirofish.zep_paging')
_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(
@ -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)}")

View File

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

View File

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

View File

@ -5,7 +5,7 @@ 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)
}
/**
@ -13,7 +13,7 @@ export const generateReport = (data) => {
* @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 } })
}
/**
@ -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 } })
}
/**
@ -31,7 +31,7 @@ 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 } })
}
/**
@ -39,7 +39,7 @@ export const getConsoleLog = (reportId, fromLine = 0) => {
* @param {string} reportId
*/
export const getReport = (reportId) => {
return service.get(`/api/report/${reportId}`)
return service.get(`/report/${reportId}`)
}
/**
@ -47,5 +47,5 @@ export const getReport = (reportId) => {
* @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)
}

View File

@ -5,7 +5,7 @@ import service, { requestWithRetry } from './index'
* @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? }
*/
export const createSimulation = (data) => {
return requestWithRetry(() => service.post('/api/simulation/create', data), 3, 1000)
return requestWithRetry(() => service.post('/simulation/create', data), 3, 1000)
}
/**
@ -13,7 +13,7 @@ export const createSimulation = (data) => {
* @param {Object} data - { simulation_id, entity_types?, use_llm_for_profiles?, parallel_profile_count?, force_regenerate? }
*/
export const prepareSimulation = (data) => {
return requestWithRetry(() => service.post('/api/simulation/prepare', data), 3, 1000)
return requestWithRetry(() => service.post('/simulation/prepare', data), 3, 1000)
}
/**
@ -21,7 +21,7 @@ export const prepareSimulation = (data) => {
* @param {Object} data - { task_id?, simulation_id? }
*/
export const getPrepareStatus = (data) => {
return service.post('/api/simulation/prepare/status', data)
return service.post('/simulation/prepare/status', data)
}
/**
@ -29,7 +29,7 @@ export const getPrepareStatus = (data) => {
* @param {string} simulationId
*/
export const getSimulation = (simulationId) => {
return service.get(`/api/simulation/${simulationId}`)
return service.get(`/simulation/${simulationId}`)
}
/**
@ -38,7 +38,7 @@ export const getSimulation = (simulationId) => {
* @param {string} platform - 'reddit' | 'twitter'
*/
export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
return service.get(`/api/simulation/${simulationId}/profiles`, { params: { platform } })
return service.get(`/simulation/${simulationId}/profiles`, { params: { platform } })
}
/**
@ -47,7 +47,7 @@ export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
* @param {string} platform - 'reddit' | 'twitter'
*/
export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit') => {
return service.get(`/api/simulation/${simulationId}/profiles/realtime`, { params: { platform } })
return service.get(`/simulation/${simulationId}/profiles/realtime`, { params: { platform } })
}
/**
@ -55,7 +55,7 @@ export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit')
* @param {string} simulationId
*/
export const getSimulationConfig = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/config`)
return service.get(`/simulation/${simulationId}/config`)
}
/**
@ -64,7 +64,7 @@ export const getSimulationConfig = (simulationId) => {
* @returns {Promise} 返回配置信息包含元数据和配置内容
*/
export const getSimulationConfigRealtime = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/config/realtime`)
return service.get(`/simulation/${simulationId}/config/realtime`)
}
/**
@ -73,7 +73,7 @@ export const getSimulationConfigRealtime = (simulationId) => {
*/
export const listSimulations = (projectId) => {
const params = projectId ? { project_id: projectId } : {}
return service.get('/api/simulation/list', { params })
return service.get('/simulation/list', { params })
}
/**
@ -81,7 +81,7 @@ export const listSimulations = (projectId) => {
* @param {Object} data - { simulation_id, platform?, max_rounds?, enable_graph_memory_update? }
*/
export const startSimulation = (data) => {
return requestWithRetry(() => service.post('/api/simulation/start', data), 3, 1000)
return requestWithRetry(() => service.post('/simulation/start', data), 3, 1000)
}
/**
@ -89,7 +89,7 @@ export const startSimulation = (data) => {
* @param {Object} data - { simulation_id }
*/
export const stopSimulation = (data) => {
return service.post('/api/simulation/stop', data)
return service.post('/simulation/stop', data)
}
/**
@ -97,7 +97,7 @@ export const stopSimulation = (data) => {
* @param {string} simulationId
*/
export const getRunStatus = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/run-status`)
return service.get(`/simulation/${simulationId}/run-status`)
}
/**
@ -105,7 +105,7 @@ export const getRunStatus = (simulationId) => {
* @param {string} simulationId
*/
export const getRunStatusDetail = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/run-status/detail`)
return service.get(`/simulation/${simulationId}/run-status/detail`)
}
/**
@ -116,7 +116,7 @@ export const getRunStatusDetail = (simulationId) => {
* @param {number} offset - 偏移量
*/
export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50, offset = 0) => {
return service.get(`/api/simulation/${simulationId}/posts`, {
return service.get(`/simulation/${simulationId}/posts`, {
params: { platform, limit, offset }
})
}
@ -132,7 +132,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
if (endRound !== null) {
params.end_round = endRound
}
return service.get(`/api/simulation/${simulationId}/timeline`, { params })
return service.get(`/simulation/${simulationId}/timeline`, { params })
}
/**
@ -140,7 +140,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
* @param {string} simulationId
*/
export const getAgentStats = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/agent-stats`)
return service.get(`/simulation/${simulationId}/agent-stats`)
}
/**
@ -149,7 +149,7 @@ export const getAgentStats = (simulationId) => {
* @param {Object} params - { limit, offset, platform, agent_id, round_num }
*/
export const getSimulationActions = (simulationId, params = {}) => {
return service.get(`/api/simulation/${simulationId}/actions`, { params })
return service.get(`/simulation/${simulationId}/actions`, { params })
}
/**
@ -157,7 +157,7 @@ export const getSimulationActions = (simulationId, params = {}) => {
* @param {Object} data - { simulation_id, timeout? }
*/
export const closeSimulationEnv = (data) => {
return service.post('/api/simulation/close-env', data)
return service.post('/simulation/close-env', data)
}
/**
@ -165,7 +165,7 @@ export const closeSimulationEnv = (data) => {
* @param {Object} data - { simulation_id }
*/
export const getEnvStatus = (data) => {
return service.post('/api/simulation/env-status', data)
return service.post('/simulation/env-status', data)
}
/**
@ -173,7 +173,7 @@ export const getEnvStatus = (data) => {
* @param {Object} data - { simulation_id, interviews: [{ agent_id, prompt }] }
*/
export const interviewAgents = (data) => {
return requestWithRetry(() => service.post('/api/simulation/interview/batch', data), 3, 1000)
return requestWithRetry(() => service.post('/simulation/interview/batch', data), 3, 1000)
}
/**
@ -182,6 +182,5 @@ export const interviewAgents = (data) => {
* @param {number} limit - 返回数量限制
*/
export const getSimulationHistory = (limit = 20) => {
return service.get('/api/simulation/history', { params: { limit } })
return service.get('/simulation/history', { params: { limit } })
}

View File

@ -105,6 +105,15 @@
</div>
</div>
<div v-if="rateLimitNotice" class="retry-notice">
<div class="retry-notice__icon"></div>
<div class="retry-notice__body">
<div class="retry-notice__title">External API rate limit detected</div>
<div class="retry-notice__text">{{ rateLimitNotice.message }}</div>
</div>
<button class="retry-notice__dismiss" @click="dismissRetryNotice">Dismiss</button>
</div>
<div class="workflow-steps" v-if="workflowSteps.length > 0">
<div
v-for="(step, sidx) in workflowSteps"
@ -418,6 +427,8 @@ const agentLogs = ref([])
const consoleLogs = ref([])
const agentLogLine = ref(0)
const consoleLogLine = ref(0)
const rateLimitNotice = ref(null)
let rateLimitNoticeTimer = null
const reportOutline = ref(null)
const currentSectionIndex = ref(null)
const generatedSections = ref({})
@ -2017,6 +2028,52 @@ const getLogLevelClass = (log) => {
return ''
}
const clearRetryNoticeTimer = () => {
if (rateLimitNoticeTimer) {
clearTimeout(rateLimitNoticeTimer)
rateLimitNoticeTimer = null
}
}
const dismissRetryNotice = () => {
rateLimitNotice.value = null
clearRetryNoticeTimer()
}
const setRetryNotice = (message, retrySeconds = null) => {
rateLimitNotice.value = {
message,
retrySeconds
}
clearRetryNoticeTimer()
if (retrySeconds && retrySeconds > 0) {
rateLimitNoticeTimer = setTimeout(() => {
rateLimitNotice.value = null
rateLimitNoticeTimer = null
}, Math.min(retrySeconds * 1000 + 1000, 120000))
}
}
const maybeHandleRetryNotice = (logLine) => {
if (!logLine) return
const text = String(logLine)
const rateLimitMatch = text.match(/rate limit hit.*retrying in ([\d.]+)s/i)
if (rateLimitMatch) {
const retrySeconds = Number.parseFloat(rateLimitMatch[1])
const retryText = Number.isFinite(retrySeconds)
? `Zep is rate limited. The system will retry automatically in about ${retrySeconds.toFixed(1)} seconds.`
: 'Zep is rate limited. The system will retry automatically.'
setRetryNotice(retryText, Number.isFinite(retrySeconds) ? retrySeconds : null)
return
}
if (text.includes('Rate limit exceeded for FREE plan') || text.includes('429')) {
setRetryNotice('Zep is rate limited. The system will retry automatically.', null)
}
}
// Polling
let agentLogTimer = null
let consoleLogTimer = null
@ -2055,6 +2112,7 @@ const fetchAgentLog = async () => {
if (log.action === 'report_complete') {
isComplete.value = true
currentSectionIndex.value = null // loading
dismissRetryNotice()
emit('update-status', 'completed')
stopPolling()
// nextTick
@ -2141,6 +2199,8 @@ const fetchConsoleLog = async () => {
if (newLogs.length > 0) {
consoleLogs.value.push(...newLogs)
consoleLogLine.value = res.data.from_line + newLogs.length
newLogs.forEach((log) => maybeHandleRetryNotice(log))
nextTick(() => {
if (logContent.value) {
@ -2185,6 +2245,7 @@ onMounted(() => {
onUnmounted(() => {
stopPolling()
clearRetryNoticeTimer()
})
watch(() => props.reportId, (newId) => {
@ -2193,6 +2254,7 @@ watch(() => props.reportId, (newId) => {
consoleLogs.value = []
agentLogLine.value = 0
consoleLogLine.value = 0
dismissRetryNotice()
reportOutline.value = null
currentSectionIndex.value = null
generatedSections.value = {}
@ -2289,6 +2351,64 @@ watch(() => props.reportId, (newId) => {
flex-shrink: 0;
}
.retry-notice {
display: flex;
align-items: flex-start;
gap: 12px;
margin: 12px 20px 0;
padding: 12px 14px;
border: 1px solid #F59E0B;
border-radius: 12px;
background: rgba(245, 158, 11, 0.08);
color: #92400E;
}
.retry-notice__icon {
width: 28px;
height: 28px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(245, 158, 11, 0.18);
flex-shrink: 0;
font-size: 16px;
}
.retry-notice__body {
flex: 1;
min-width: 0;
}
.retry-notice__title {
font-size: 13px;
font-weight: 700;
line-height: 1.2;
}
.retry-notice__text {
margin-top: 4px;
font-size: 12px;
line-height: 1.5;
color: #B45309;
}
.retry-notice__dismiss {
flex-shrink: 0;
border: 0;
border-radius: 999px;
padding: 6px 10px;
background: rgba(245, 158, 11, 0.16);
color: #92400E;
cursor: pointer;
font-size: 12px;
font-weight: 600;
}
.retry-notice__dismiss:hover {
background: rgba(245, 158, 11, 0.24);
}
/* Panel header status variants */
.panel-header--active {
background: #FAFAFA;

View File

@ -13,7 +13,6 @@ export default defineConfig({
},
server: {
port: 3000,
open: true,
proxy: {
'/api': {
target: 'http://localhost:5001',