Merge 69e7cbacea into 96096ea0ff
This commit is contained in:
commit
6f209ecb3c
|
|
@ -10,10 +10,12 @@ Zep检索工具服务
|
||||||
|
|
||||||
import time
|
import time
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from zep_cloud.client import Zep
|
from zep_cloud.client import Zep
|
||||||
|
from zep_cloud.core.api_error import ApiError
|
||||||
|
|
||||||
from ..config import Config
|
from ..config import Config
|
||||||
from ..utils.logger import get_logger
|
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')
|
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
|
@dataclass
|
||||||
class SearchResult:
|
class SearchResult:
|
||||||
|
|
@ -448,14 +499,32 @@ class ZepToolsService:
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
return func()
|
return func()
|
||||||
except Exception as e:
|
except (Exception, ApiError) as e:
|
||||||
last_exception = e
|
last_exception = e
|
||||||
if attempt < max_retries - 1:
|
if attempt < max_retries - 1:
|
||||||
logger.warning(
|
if _is_rate_limit_error(e):
|
||||||
t("console.zepRetryAttempt", operation=operation_name, attempt=attempt + 1, error=str(e)[:100], delay=f"{delay:.1f}")
|
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)
|
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:
|
else:
|
||||||
logger.error(t("console.zepAllRetriesFailed", operation=operation_name, retries=max_retries, error=str(e)))
|
logger.error(t("console.zepAllRetriesFailed", operation=operation_name, retries=max_retries, error=str(e)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ Zep 的 node/edge 列表接口使用 UUID cursor 分页,
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from zep_cloud import InternalServerError
|
from zep_cloud import InternalServerError
|
||||||
|
from zep_cloud.core.api_error import ApiError
|
||||||
from zep_cloud.client import Zep
|
from zep_cloud.client import Zep
|
||||||
|
|
||||||
from .logger import get_logger
|
from .logger import get_logger
|
||||||
|
|
@ -19,8 +21,57 @@ logger = get_logger('mirofish.zep_paging')
|
||||||
|
|
||||||
_DEFAULT_PAGE_SIZE = 100
|
_DEFAULT_PAGE_SIZE = 100
|
||||||
_MAX_NODES = 2000
|
_MAX_NODES = 2000
|
||||||
_DEFAULT_MAX_RETRIES = 3
|
_DEFAULT_MAX_RETRIES = 5
|
||||||
_DEFAULT_RETRY_DELAY = 2.0 # seconds, doubles each retry
|
_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(
|
def _fetch_page_with_retry(
|
||||||
|
|
@ -41,14 +92,33 @@ def _fetch_page_with_retry(
|
||||||
for attempt in range(max_retries):
|
for attempt in range(max_retries):
|
||||||
try:
|
try:
|
||||||
return api_call(*args, **kwargs)
|
return api_call(*args, **kwargs)
|
||||||
except (ConnectionError, TimeoutError, OSError, InternalServerError) as e:
|
except (ConnectionError, TimeoutError, OSError, InternalServerError, ApiError) as e:
|
||||||
last_exception = e
|
last_exception = e
|
||||||
if attempt < max_retries - 1:
|
if attempt < max_retries - 1:
|
||||||
logger.warning(
|
if _is_rate_limit_error(e):
|
||||||
f"Zep {page_description} attempt {attempt + 1} failed: {str(e)[:100]}, retrying in {delay:.1f}s..."
|
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)
|
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:
|
else:
|
||||||
logger.error(f"Zep {page_description} failed after {max_retries} attempts: {str(e)}")
|
logger.error(f"Zep {page_description} failed after {max_retries} attempts: {str(e)}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,9 @@ import service, { requestWithRetry } from './index'
|
||||||
export function generateOntology(formData) {
|
export function generateOntology(formData) {
|
||||||
return requestWithRetry(() =>
|
return requestWithRetry(() =>
|
||||||
service({
|
service({
|
||||||
url: '/api/graph/ontology/generate',
|
url: '/graph/ontology/generate',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data: formData,
|
data: formData
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data'
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -26,7 +23,7 @@ export function generateOntology(formData) {
|
||||||
export function buildGraph(data) {
|
export function buildGraph(data) {
|
||||||
return requestWithRetry(() =>
|
return requestWithRetry(() =>
|
||||||
service({
|
service({
|
||||||
url: '/api/graph/build',
|
url: '/graph/build',
|
||||||
method: 'post',
|
method: 'post',
|
||||||
data
|
data
|
||||||
})
|
})
|
||||||
|
|
@ -40,7 +37,7 @@ export function buildGraph(data) {
|
||||||
*/
|
*/
|
||||||
export function getTaskStatus(taskId) {
|
export function getTaskStatus(taskId) {
|
||||||
return service({
|
return service({
|
||||||
url: `/api/graph/task/${taskId}`,
|
url: `/graph/task/${taskId}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +49,7 @@ export function getTaskStatus(taskId) {
|
||||||
*/
|
*/
|
||||||
export function getGraphData(graphId) {
|
export function getGraphData(graphId) {
|
||||||
return service({
|
return service({
|
||||||
url: `/api/graph/data/${graphId}`,
|
url: `/graph/data/${graphId}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +61,7 @@ export function getGraphData(graphId) {
|
||||||
*/
|
*/
|
||||||
export function getProject(projectId) {
|
export function getProject(projectId) {
|
||||||
return service({
|
return service({
|
||||||
url: `/api/graph/project/${projectId}`,
|
url: `/graph/project/${projectId}`,
|
||||||
method: 'get'
|
method: 'get'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,19 @@ import i18n from '../i18n'
|
||||||
|
|
||||||
// 创建axios实例
|
// 创建axios实例
|
||||||
const service = axios.create({
|
const service = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5001',
|
// Default to the Vite proxy so the frontend works both on localhost
|
||||||
timeout: 300000, // 5分钟超时(本体生成可能需要较长时间)
|
// and when accessed from another machine via the dev server host.
|
||||||
headers: {
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||||
'Content-Type': 'application/json'
|
timeout: 300000 // 5分钟超时(本体生成可能需要较长时间)
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
config.headers['Accept-Language'] = i18n.global.locale.value
|
config.headers['Accept-Language'] = i18n.global.locale.value
|
||||||
|
if (config.data instanceof FormData) {
|
||||||
|
delete config.headers['Content-Type']
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import service, { requestWithRetry } from './index'
|
||||||
* @param {Object} data - { simulation_id, force_regenerate? }
|
* @param {Object} data - { simulation_id, force_regenerate? }
|
||||||
*/
|
*/
|
||||||
export const generateReport = (data) => {
|
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
|
* @param {string} reportId
|
||||||
*/
|
*/
|
||||||
export const getReportStatus = (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 - 从第几行开始获取
|
* @param {number} fromLine - 从第几行开始获取
|
||||||
*/
|
*/
|
||||||
export const getAgentLog = (reportId, fromLine = 0) => {
|
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 - 从第几行开始获取
|
* @param {number} fromLine - 从第几行开始获取
|
||||||
*/
|
*/
|
||||||
export const getConsoleLog = (reportId, fromLine = 0) => {
|
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
|
* @param {string} reportId
|
||||||
*/
|
*/
|
||||||
export const getReport = (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? }
|
* @param {Object} data - { simulation_id, message, chat_history? }
|
||||||
*/
|
*/
|
||||||
export const chatWithReport = (data) => {
|
export const chatWithReport = (data) => {
|
||||||
return requestWithRetry(() => service.post('/api/report/chat', data), 3, 1000)
|
return requestWithRetry(() => service.post('/report/chat', data), 3, 1000)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import service, { requestWithRetry } from './index'
|
||||||
* @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? }
|
* @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? }
|
||||||
*/
|
*/
|
||||||
export const createSimulation = (data) => {
|
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? }
|
* @param {Object} data - { simulation_id, entity_types?, use_llm_for_profiles?, parallel_profile_count?, force_regenerate? }
|
||||||
*/
|
*/
|
||||||
export const prepareSimulation = (data) => {
|
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? }
|
* @param {Object} data - { task_id?, simulation_id? }
|
||||||
*/
|
*/
|
||||||
export const getPrepareStatus = (data) => {
|
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
|
* @param {string} simulationId
|
||||||
*/
|
*/
|
||||||
export const getSimulation = (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'
|
* @param {string} platform - 'reddit' | 'twitter'
|
||||||
*/
|
*/
|
||||||
export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
|
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'
|
* @param {string} platform - 'reddit' | 'twitter'
|
||||||
*/
|
*/
|
||||||
export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit') => {
|
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
|
* @param {string} simulationId
|
||||||
*/
|
*/
|
||||||
export const getSimulationConfig = (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} 返回配置信息,包含元数据和配置内容
|
* @returns {Promise} 返回配置信息,包含元数据和配置内容
|
||||||
*/
|
*/
|
||||||
export const getSimulationConfigRealtime = (simulationId) => {
|
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) => {
|
export const listSimulations = (projectId) => {
|
||||||
const params = projectId ? { project_id: 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? }
|
* @param {Object} data - { simulation_id, platform?, max_rounds?, enable_graph_memory_update? }
|
||||||
*/
|
*/
|
||||||
export const startSimulation = (data) => {
|
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 }
|
* @param {Object} data - { simulation_id }
|
||||||
*/
|
*/
|
||||||
export const stopSimulation = (data) => {
|
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
|
* @param {string} simulationId
|
||||||
*/
|
*/
|
||||||
export const getRunStatus = (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
|
* @param {string} simulationId
|
||||||
*/
|
*/
|
||||||
export const getRunStatusDetail = (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 - 偏移量
|
* @param {number} offset - 偏移量
|
||||||
*/
|
*/
|
||||||
export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50, offset = 0) => {
|
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 }
|
params: { platform, limit, offset }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +132,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
|
||||||
if (endRound !== null) {
|
if (endRound !== null) {
|
||||||
params.end_round = endRound
|
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
|
* @param {string} simulationId
|
||||||
*/
|
*/
|
||||||
export const getAgentStats = (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 }
|
* @param {Object} params - { limit, offset, platform, agent_id, round_num }
|
||||||
*/
|
*/
|
||||||
export const getSimulationActions = (simulationId, params = {}) => {
|
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? }
|
* @param {Object} data - { simulation_id, timeout? }
|
||||||
*/
|
*/
|
||||||
export const closeSimulationEnv = (data) => {
|
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 }
|
* @param {Object} data - { simulation_id }
|
||||||
*/
|
*/
|
||||||
export const getEnvStatus = (data) => {
|
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 }] }
|
* @param {Object} data - { simulation_id, interviews: [{ agent_id, prompt }] }
|
||||||
*/
|
*/
|
||||||
export const interviewAgents = (data) => {
|
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 - 返回数量限制
|
* @param {number} limit - 返回数量限制
|
||||||
*/
|
*/
|
||||||
export const getSimulationHistory = (limit = 20) => {
|
export const getSimulationHistory = (limit = 20) => {
|
||||||
return service.get('/api/simulation/history', { params: { limit } })
|
return service.get('/simulation/history', { params: { limit } })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,15 @@
|
||||||
</div>
|
</div>
|
||||||
</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 class="workflow-steps" v-if="workflowSteps.length > 0">
|
||||||
<div
|
<div
|
||||||
v-for="(step, sidx) in workflowSteps"
|
v-for="(step, sidx) in workflowSteps"
|
||||||
|
|
@ -418,6 +427,8 @@ const agentLogs = ref([])
|
||||||
const consoleLogs = ref([])
|
const consoleLogs = ref([])
|
||||||
const agentLogLine = ref(0)
|
const agentLogLine = ref(0)
|
||||||
const consoleLogLine = ref(0)
|
const consoleLogLine = ref(0)
|
||||||
|
const rateLimitNotice = ref(null)
|
||||||
|
let rateLimitNoticeTimer = null
|
||||||
const reportOutline = ref(null)
|
const reportOutline = ref(null)
|
||||||
const currentSectionIndex = ref(null)
|
const currentSectionIndex = ref(null)
|
||||||
const generatedSections = ref({})
|
const generatedSections = ref({})
|
||||||
|
|
@ -2017,6 +2028,52 @@ const getLogLevelClass = (log) => {
|
||||||
return ''
|
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
|
// Polling
|
||||||
let agentLogTimer = null
|
let agentLogTimer = null
|
||||||
let consoleLogTimer = null
|
let consoleLogTimer = null
|
||||||
|
|
@ -2055,6 +2112,7 @@ const fetchAgentLog = async () => {
|
||||||
if (log.action === 'report_complete') {
|
if (log.action === 'report_complete') {
|
||||||
isComplete.value = true
|
isComplete.value = true
|
||||||
currentSectionIndex.value = null // 确保清除 loading 状态
|
currentSectionIndex.value = null // 确保清除 loading 状态
|
||||||
|
dismissRetryNotice()
|
||||||
emit('update-status', 'completed')
|
emit('update-status', 'completed')
|
||||||
stopPolling()
|
stopPolling()
|
||||||
// 滚动逻辑统一在循环结束后的 nextTick 中处理
|
// 滚动逻辑统一在循环结束后的 nextTick 中处理
|
||||||
|
|
@ -2141,6 +2199,8 @@ const fetchConsoleLog = async () => {
|
||||||
if (newLogs.length > 0) {
|
if (newLogs.length > 0) {
|
||||||
consoleLogs.value.push(...newLogs)
|
consoleLogs.value.push(...newLogs)
|
||||||
consoleLogLine.value = res.data.from_line + newLogs.length
|
consoleLogLine.value = res.data.from_line + newLogs.length
|
||||||
|
|
||||||
|
newLogs.forEach((log) => maybeHandleRetryNotice(log))
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (logContent.value) {
|
if (logContent.value) {
|
||||||
|
|
@ -2185,6 +2245,7 @@ onMounted(() => {
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopPolling()
|
stopPolling()
|
||||||
|
clearRetryNoticeTimer()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => props.reportId, (newId) => {
|
watch(() => props.reportId, (newId) => {
|
||||||
|
|
@ -2193,6 +2254,7 @@ watch(() => props.reportId, (newId) => {
|
||||||
consoleLogs.value = []
|
consoleLogs.value = []
|
||||||
agentLogLine.value = 0
|
agentLogLine.value = 0
|
||||||
consoleLogLine.value = 0
|
consoleLogLine.value = 0
|
||||||
|
dismissRetryNotice()
|
||||||
reportOutline.value = null
|
reportOutline.value = null
|
||||||
currentSectionIndex.value = null
|
currentSectionIndex.value = null
|
||||||
generatedSections.value = {}
|
generatedSections.value = {}
|
||||||
|
|
@ -2289,6 +2351,64 @@ watch(() => props.reportId, (newId) => {
|
||||||
flex-shrink: 0;
|
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 status variants */
|
||||||
.panel-header--active {
|
.panel-header--active {
|
||||||
background: #FAFAFA;
|
background: #FAFAFA;
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
open: true,
|
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://localhost:5001',
|
target: 'http://localhost:5001',
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue