feat: refactor API key configuration and enhance application initialization
- Renamed `check_environment` to `check_api_key_configured` for clarity, simplifying the API key validation logic. - Removed the blocking behavior of the API key check during application startup, allowing the app to run while providing a prompt for configuration. - Updated `LocalAgentApp` to accept an `api_configured` parameter, enabling conditional messaging for API key setup. - Enhanced the `SandboxRunner` to support backup management and improved execution result handling with detailed metrics. - Integrated data governance strategies into the `HistoryManager`, ensuring compliance and improved data management. - Added privacy settings and metrics tracking across various components to enhance user experience and application safety.
This commit is contained in:
352
llm/client.py
352
llm/client.py
@@ -12,15 +12,48 @@ import requests
|
||||
from pathlib import Path
|
||||
from typing import Optional, Generator, Callable, List, Dict, Any
|
||||
from dotenv import load_dotenv
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
# 获取项目根目录
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
ENV_PATH = PROJECT_ROOT / ".env"
|
||||
|
||||
# 配置日志目录
|
||||
LOGS_DIR = PROJECT_ROOT / "workspace" / "logs"
|
||||
LOGS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 配置日志记录器
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 创建文件处理器 - 按日期命名
|
||||
log_file = LOGS_DIR / f"llm_calls_{datetime.now().strftime('%Y%m%d')}.log"
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# 设置日志格式
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# 添加处理器
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
|
||||
class LLMClientError(Exception):
|
||||
"""LLM 客户端异常"""
|
||||
pass
|
||||
|
||||
# 异常类型分类
|
||||
TYPE_NETWORK = "network" # 网络错误(超时、连接失败等)
|
||||
TYPE_SERVER = "server" # 服务器错误(5xx)
|
||||
TYPE_CLIENT = "client" # 客户端错误(4xx)
|
||||
TYPE_PARSE = "parse" # 解析错误
|
||||
TYPE_CONFIG = "config" # 配置错误
|
||||
|
||||
def __init__(self, message: str, error_type: str = TYPE_CLIENT, original_exception: Optional[Exception] = None):
|
||||
super().__init__(message)
|
||||
self.error_type = error_type
|
||||
self.original_exception = original_exception
|
||||
|
||||
|
||||
class LLMClient:
|
||||
@@ -61,21 +94,38 @@ class LLMClient:
|
||||
self.max_retries = max_retries
|
||||
|
||||
if not self.api_url:
|
||||
raise LLMClientError("未配置 LLM_API_URL,请检查 .env 文件")
|
||||
raise LLMClientError("未配置 LLM_API_URL,请检查 .env 文件", error_type=LLMClientError.TYPE_CONFIG)
|
||||
if not self.api_key or self.api_key == "your_api_key_here":
|
||||
raise LLMClientError("未配置有效的 LLM_API_KEY,请检查 .env 文件")
|
||||
raise LLMClientError("未配置有效的 LLM_API_KEY,请检查 .env 文件", error_type=LLMClientError.TYPE_CONFIG)
|
||||
|
||||
def _should_retry(self, exception: Exception) -> bool:
|
||||
"""判断是否应该重试"""
|
||||
# 网络连接错误、超时错误可以重试
|
||||
"""
|
||||
判断是否应该重试
|
||||
|
||||
可重试的异常类型:
|
||||
- 网络错误(超时、连接失败)
|
||||
- 服务器错误(5xx)
|
||||
- 限流错误(429)
|
||||
"""
|
||||
# 直接的网络异常(理论上不应该到这里,但保留作为兜底)
|
||||
if isinstance(exception, (requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout)):
|
||||
return True
|
||||
# 服务器错误(5xx)可以重试
|
||||
|
||||
# LLMClientError 根据错误类型判断
|
||||
if isinstance(exception, LLMClientError):
|
||||
error_msg = str(exception)
|
||||
if "状态码: 5" in error_msg or "502" in error_msg or "503" in error_msg or "504" in error_msg:
|
||||
# 网络错误和服务器错误可以重试
|
||||
if exception.error_type in (LLMClientError.TYPE_NETWORK, LLMClientError.TYPE_SERVER):
|
||||
return True
|
||||
|
||||
# 检查原始异常
|
||||
if exception.original_exception:
|
||||
if isinstance(exception.original_exception,
|
||||
(requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout,
|
||||
requests.exceptions.ChunkedEncodingError)):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _do_request_with_retry(
|
||||
@@ -85,20 +135,60 @@ class LLMClient:
|
||||
):
|
||||
"""带重试的请求执行"""
|
||||
last_exception = None
|
||||
retry_count = 0
|
||||
|
||||
for attempt in range(self.max_retries + 1):
|
||||
try:
|
||||
return request_func()
|
||||
result = request_func()
|
||||
|
||||
# 记录成功的请求(包括重试后成功)
|
||||
if retry_count > 0:
|
||||
try:
|
||||
from llm.config_metrics import get_config_metrics
|
||||
workspace = PROJECT_ROOT / "workspace"
|
||||
if workspace.exists():
|
||||
metrics = get_config_metrics(workspace)
|
||||
metrics.record_retry_success(retry_count)
|
||||
except:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
last_exception = e
|
||||
|
||||
# 判断是否应该重试
|
||||
if attempt < self.max_retries and self._should_retry(e):
|
||||
retry_count += 1
|
||||
delay = self.DEFAULT_RETRY_DELAY * (self.DEFAULT_RETRY_BACKOFF ** attempt)
|
||||
print(f"[重试] {operation_name}失败,{delay:.1f}秒后重试 ({attempt + 1}/{self.max_retries})...")
|
||||
|
||||
# 记录重试信息
|
||||
error_type = getattr(e, 'error_type', 'unknown') if isinstance(e, LLMClientError) else type(e).__name__
|
||||
print(f"[重试] {operation_name}失败 (错误类型: {error_type}),{delay:.1f}秒后重试 ({attempt + 1}/{self.max_retries})...")
|
||||
|
||||
# 记录重试次数到配置度量
|
||||
try:
|
||||
from llm.config_metrics import get_config_metrics
|
||||
workspace = PROJECT_ROOT / "workspace"
|
||||
if workspace.exists():
|
||||
metrics = get_config_metrics(workspace)
|
||||
metrics.increment_retry()
|
||||
except:
|
||||
pass # 度量记录失败不影响主流程
|
||||
|
||||
time.sleep(delay)
|
||||
continue
|
||||
else:
|
||||
# 记录最终失败
|
||||
if retry_count > 0:
|
||||
try:
|
||||
from llm.config_metrics import get_config_metrics
|
||||
workspace = PROJECT_ROOT / "workspace"
|
||||
if workspace.exists():
|
||||
metrics = get_config_metrics(workspace)
|
||||
metrics.record_retry_failure(retry_count)
|
||||
except:
|
||||
pass
|
||||
raise
|
||||
|
||||
# 所有重试都失败
|
||||
@@ -125,6 +215,22 @@ class LLMClient:
|
||||
Returns:
|
||||
LLM 生成的文本内容
|
||||
"""
|
||||
# 记录输入 - 完整内容不截断
|
||||
logger.info("=" * 80)
|
||||
logger.info(f"LLM 调用 [非流式] - 模型: {model}")
|
||||
logger.info(f"参数: temperature={temperature}, max_tokens={max_tokens}, timeout={timeout}s")
|
||||
logger.info(f"时间戳: {datetime.now().isoformat()}")
|
||||
logger.info("-" * 80)
|
||||
logger.info("输入消息:")
|
||||
for i, msg in enumerate(messages):
|
||||
role = msg.get('role', 'unknown')
|
||||
content = msg.get('content', '')
|
||||
logger.info(f" [{i+1}] {role} ({len(content)} 字符):")
|
||||
# 完整记录,不截断
|
||||
for line in content.split('\n'):
|
||||
logger.info(f" {line}")
|
||||
logger.info("-" * 80)
|
||||
|
||||
def do_request():
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
@@ -139,36 +245,85 @@ class LLMClient:
|
||||
"max_tokens": max_tokens
|
||||
}
|
||||
|
||||
# 记录请求详情
|
||||
logger.debug(f"API URL: {self.api_url}")
|
||||
logger.debug(f"请求 Payload: {json.dumps(payload, ensure_ascii=False, indent=2)}")
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = requests.post(
|
||||
self.api_url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=timeout
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
raise LLMClientError(f"请求超时({timeout}秒),请检查网络连接或稍后重试")
|
||||
except requests.exceptions.ConnectionError:
|
||||
raise LLMClientError("网络连接失败,请检查网络设置")
|
||||
elapsed_time = time.time() - start_time
|
||||
logger.info(f"请求耗时: {elapsed_time:.2f}秒")
|
||||
except requests.exceptions.Timeout as e:
|
||||
logger.error(f"请求超时: {timeout}秒")
|
||||
raise LLMClientError(
|
||||
f"请求超时({timeout}秒),请检查网络连接或稍后重试",
|
||||
error_type=LLMClientError.TYPE_NETWORK,
|
||||
original_exception=e
|
||||
)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(f"网络连接失败: {str(e)}")
|
||||
raise LLMClientError(
|
||||
"网络连接失败,请检查网络设置",
|
||||
error_type=LLMClientError.TYPE_NETWORK,
|
||||
original_exception=e
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise LLMClientError(f"网络请求异常: {str(e)}")
|
||||
logger.error(f"网络请求异常: {str(e)}")
|
||||
raise LLMClientError(
|
||||
f"网络请求异常: {str(e)}",
|
||||
error_type=LLMClientError.TYPE_NETWORK,
|
||||
original_exception=e
|
||||
)
|
||||
|
||||
# 记录响应状态
|
||||
logger.debug(f"响应状态码: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
error_msg = f"API 返回错误 (状态码: {response.status_code})"
|
||||
try:
|
||||
error_detail = response.json()
|
||||
logger.error(f"错误详情: {json.dumps(error_detail, ensure_ascii=False, indent=2)}")
|
||||
if "error" in error_detail:
|
||||
error_msg += f": {error_detail['error']}"
|
||||
except:
|
||||
logger.error(f"错误响应: {response.text[:500]}")
|
||||
error_msg += f": {response.text[:200]}"
|
||||
raise LLMClientError(error_msg)
|
||||
|
||||
# 根据状态码确定错误类型
|
||||
if response.status_code >= 500:
|
||||
error_type = LLMClientError.TYPE_SERVER
|
||||
elif response.status_code == 429:
|
||||
error_type = LLMClientError.TYPE_SERVER # 限流也可重试
|
||||
else:
|
||||
error_type = LLMClientError.TYPE_CLIENT
|
||||
|
||||
raise LLMClientError(error_msg, error_type=error_type)
|
||||
|
||||
try:
|
||||
result = response.json()
|
||||
content = result["choices"][0]["message"]["content"]
|
||||
|
||||
# 记录输出 - 完整内容不截断
|
||||
logger.info("输出响应:")
|
||||
logger.info(f" 长度: {len(content)} 字符")
|
||||
for line in content.split('\n'):
|
||||
logger.info(f" {line}")
|
||||
logger.info("=" * 80)
|
||||
|
||||
return content
|
||||
except (KeyError, IndexError, TypeError) as e:
|
||||
raise LLMClientError(f"解析 API 响应失败: {str(e)}")
|
||||
logger.error(f"解析 API 响应失败: {str(e)}")
|
||||
logger.error(f"原始响应: {response.text[:1000]}")
|
||||
raise LLMClientError(
|
||||
f"解析 API 响应失败: {str(e)}",
|
||||
error_type=LLMClientError.TYPE_PARSE
|
||||
)
|
||||
|
||||
return self._do_request_with_retry(do_request, "LLM调用")
|
||||
|
||||
@@ -193,6 +348,23 @@ class LLMClient:
|
||||
Yields:
|
||||
逐个返回生成的文本片段
|
||||
"""
|
||||
# 记录输入 - 完整内容不截断
|
||||
logger.info("=" * 80)
|
||||
logger.info(f"LLM 调用 [流式] - 模型: {model}")
|
||||
logger.info(f"参数: temperature={temperature}, max_tokens={max_tokens}, timeout={timeout}s")
|
||||
logger.info(f"时间戳: {datetime.now().isoformat()}")
|
||||
logger.info("-" * 80)
|
||||
logger.info("输入消息:")
|
||||
for i, msg in enumerate(messages):
|
||||
role = msg.get('role', 'unknown')
|
||||
content = msg.get('content', '')
|
||||
logger.info(f" [{i+1}] {role} ({len(content)} 字符):")
|
||||
# 完整记录,不截断
|
||||
for line in content.split('\n'):
|
||||
logger.info(f" {line}")
|
||||
logger.info("-" * 80)
|
||||
logger.info("开始接收流式输出...")
|
||||
|
||||
def do_request():
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
@@ -207,7 +379,12 @@ class LLMClient:
|
||||
"max_tokens": max_tokens
|
||||
}
|
||||
|
||||
# 记录请求详情
|
||||
logger.debug(f"API URL: {self.api_url}")
|
||||
logger.debug(f"请求 Payload: {json.dumps(payload, ensure_ascii=False, indent=2)}")
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
response = requests.post(
|
||||
self.api_url,
|
||||
headers=headers,
|
||||
@@ -215,45 +392,92 @@ class LLMClient:
|
||||
timeout=timeout,
|
||||
stream=True
|
||||
)
|
||||
except requests.exceptions.Timeout:
|
||||
raise LLMClientError(f"请求超时({timeout}秒),请检查网络连接或稍后重试")
|
||||
except requests.exceptions.ConnectionError:
|
||||
raise LLMClientError("网络连接失败,请检查网络设置")
|
||||
elapsed_time = time.time() - start_time
|
||||
logger.info(f"连接建立耗时: {elapsed_time:.2f}秒")
|
||||
except requests.exceptions.Timeout as e:
|
||||
logger.error(f"请求超时: {timeout}秒")
|
||||
raise LLMClientError(
|
||||
f"请求超时({timeout}秒),请检查网络连接或稍后重试",
|
||||
error_type=LLMClientError.TYPE_NETWORK,
|
||||
original_exception=e
|
||||
)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
logger.error(f"网络连接失败: {str(e)}")
|
||||
raise LLMClientError(
|
||||
"网络连接失败,请检查网络设置",
|
||||
error_type=LLMClientError.TYPE_NETWORK,
|
||||
original_exception=e
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise LLMClientError(f"网络请求异常: {str(e)}")
|
||||
logger.error(f"网络请求异常: {str(e)}")
|
||||
raise LLMClientError(
|
||||
f"网络请求异常: {str(e)}",
|
||||
error_type=LLMClientError.TYPE_NETWORK,
|
||||
original_exception=e
|
||||
)
|
||||
|
||||
# 记录响应状态
|
||||
logger.debug(f"响应状态码: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
error_msg = f"API 返回错误 (状态码: {response.status_code})"
|
||||
try:
|
||||
error_detail = response.json()
|
||||
logger.error(f"错误详情: {json.dumps(error_detail, ensure_ascii=False, indent=2)}")
|
||||
if "error" in error_detail:
|
||||
error_msg += f": {error_detail['error']}"
|
||||
except:
|
||||
logger.error(f"错误响应: {response.text[:500]}")
|
||||
error_msg += f": {response.text[:200]}"
|
||||
raise LLMClientError(error_msg)
|
||||
|
||||
# 根据状态码确定错误类型
|
||||
if response.status_code >= 500:
|
||||
error_type = LLMClientError.TYPE_SERVER
|
||||
elif response.status_code == 429:
|
||||
error_type = LLMClientError.TYPE_SERVER # 限流也可重试
|
||||
else:
|
||||
error_type = LLMClientError.TYPE_CLIENT
|
||||
|
||||
raise LLMClientError(error_msg, error_type=error_type)
|
||||
|
||||
return response
|
||||
|
||||
# 流式请求的重试只在建立连接阶段
|
||||
response = self._do_request_with_retry(do_request, "流式LLM调用")
|
||||
|
||||
# 收集完整输出用于日志
|
||||
full_output = []
|
||||
|
||||
# 解析 SSE 流
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
line = line.decode('utf-8')
|
||||
if line.startswith('data: '):
|
||||
data = line[6:] # 去掉 "data: " 前缀
|
||||
if data == '[DONE]':
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
if 'choices' in chunk and len(chunk['choices']) > 0:
|
||||
delta = chunk['choices'][0].get('delta', {})
|
||||
content = delta.get('content', '')
|
||||
if content:
|
||||
yield content
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
try:
|
||||
for line in response.iter_lines():
|
||||
if line:
|
||||
line = line.decode('utf-8')
|
||||
if line.startswith('data: '):
|
||||
data = line[6:] # 去掉 "data: " 前缀
|
||||
if data == '[DONE]':
|
||||
break
|
||||
try:
|
||||
chunk = json.loads(data)
|
||||
if 'choices' in chunk and len(chunk['choices']) > 0:
|
||||
delta = chunk['choices'][0].get('delta', {})
|
||||
content = delta.get('content', '')
|
||||
if content:
|
||||
full_output.append(content)
|
||||
yield content
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"流式输出异常: {str(e)}")
|
||||
raise
|
||||
|
||||
# 记录完整输出 - 不截断
|
||||
complete_output = ''.join(full_output)
|
||||
logger.info("流式输出完成:")
|
||||
logger.info(f" 总长度: {len(complete_output)} 字符")
|
||||
for line in complete_output.split('\n'):
|
||||
logger.info(f" {line}")
|
||||
logger.info("=" * 80)
|
||||
|
||||
def chat_stream_collect(
|
||||
self,
|
||||
@@ -304,3 +528,53 @@ def get_client() -> LLMClient:
|
||||
if _client is None:
|
||||
_client = LLMClient()
|
||||
return _client
|
||||
|
||||
|
||||
def reset_client() -> None:
|
||||
"""重置 LLM 客户端单例(配置变更后调用)"""
|
||||
global _client
|
||||
_client = None
|
||||
|
||||
|
||||
def test_connection(timeout: int = 10) -> tuple[bool, str]:
|
||||
"""
|
||||
测试 API 连接是否正常
|
||||
|
||||
Args:
|
||||
timeout: 超时时间(秒)
|
||||
|
||||
Returns:
|
||||
(是否成功, 消息)
|
||||
"""
|
||||
try:
|
||||
client = get_client()
|
||||
|
||||
# 发送简单的测试请求
|
||||
response = client.chat(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
model=os.getenv("INTENT_MODEL_NAME") or "Qwen/Qwen2.5-7B-Instruct",
|
||||
temperature=0.1,
|
||||
max_tokens=10,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
return (True, "连接成功")
|
||||
|
||||
except LLMClientError as e:
|
||||
error_msg = str(e)
|
||||
if "未配置" in error_msg or "API Key" in error_msg:
|
||||
return (False, f"配置错误: {error_msg}")
|
||||
elif "状态码: 401" in error_msg or "Unauthorized" in error_msg:
|
||||
return (False, "API Key 无效,请检查配置")
|
||||
elif "状态码: 403" in error_msg:
|
||||
return (False, "API Key 权限不足")
|
||||
elif "状态码: 404" in error_msg:
|
||||
return (False, "API 地址错误或模型不存在")
|
||||
elif "网络连接失败" in error_msg:
|
||||
return (False, "网络连接失败,请检查网络设置")
|
||||
elif "请求超时" in error_msg:
|
||||
return (False, f"连接超时({timeout}秒),请检查网络或稍后重试")
|
||||
else:
|
||||
return (False, f"连接失败: {error_msg}")
|
||||
except Exception as e:
|
||||
return (False, f"未知错误: {str(e)}")
|
||||
|
||||
167
llm/config_metrics.py
Normal file
167
llm/config_metrics.py
Normal file
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
配置变更度量模块
|
||||
跟踪配置保存后的首次调用成功率和重试次数
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
from dataclasses import dataclass, asdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConfigChangeMetric:
|
||||
"""配置变更度量记录"""
|
||||
timestamp: str
|
||||
config_changed: bool # 是否发生配置变更
|
||||
first_call_success: Optional[bool] # 首次调用是否成功
|
||||
retry_count: int # 重试次数
|
||||
error_message: Optional[str] # 错误信息
|
||||
connection_test_success: bool # 保存后连通性测试是否成功
|
||||
time_to_success_ms: Optional[int] # 从配置变更到首次成功调用的时间(毫秒)
|
||||
|
||||
|
||||
class ConfigMetricsManager:
|
||||
"""配置度量管理器"""
|
||||
|
||||
def __init__(self, metrics_file: Path):
|
||||
self.metrics_file = metrics_file
|
||||
self.metrics_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 当前配置变更状态
|
||||
self._config_changed = False
|
||||
self._config_change_time: Optional[datetime] = None
|
||||
self._connection_test_success = False
|
||||
self._first_call_recorded = False
|
||||
self._retry_count = 0
|
||||
|
||||
def mark_config_changed(self, connection_test_success: bool) -> None:
|
||||
"""标记配置已变更"""
|
||||
self._config_changed = True
|
||||
self._config_change_time = datetime.now()
|
||||
self._connection_test_success = connection_test_success
|
||||
self._first_call_recorded = False
|
||||
self._retry_count = 0
|
||||
|
||||
def record_first_call(self, success: bool, error_message: Optional[str] = None) -> None:
|
||||
"""记录配置变更后的首次调用"""
|
||||
if not self._config_changed or self._first_call_recorded:
|
||||
return
|
||||
|
||||
time_to_success_ms = None
|
||||
if self._config_change_time:
|
||||
delta = datetime.now() - self._config_change_time
|
||||
time_to_success_ms = int(delta.total_seconds() * 1000)
|
||||
|
||||
metric = ConfigChangeMetric(
|
||||
timestamp=datetime.now().isoformat(),
|
||||
config_changed=True,
|
||||
first_call_success=success,
|
||||
retry_count=self._retry_count,
|
||||
error_message=error_message,
|
||||
connection_test_success=self._connection_test_success,
|
||||
time_to_success_ms=time_to_success_ms
|
||||
)
|
||||
|
||||
self._save_metric(metric)
|
||||
self._first_call_recorded = True
|
||||
|
||||
# 如果成功,重置状态
|
||||
if success:
|
||||
self._config_changed = False
|
||||
self._retry_count = 0
|
||||
|
||||
def increment_retry(self) -> None:
|
||||
"""增加重试计数"""
|
||||
if self._config_changed:
|
||||
self._retry_count += 1
|
||||
|
||||
def record_retry_success(self, retry_count: int) -> None:
|
||||
"""记录重试后成功的请求"""
|
||||
# 可以用于统计重试恢复率
|
||||
pass
|
||||
|
||||
def record_retry_failure(self, retry_count: int) -> None:
|
||||
"""记录重试后仍失败的请求"""
|
||||
# 可以用于统计重试失败率
|
||||
pass
|
||||
|
||||
def _save_metric(self, metric: ConfigChangeMetric) -> None:
|
||||
"""保存度量记录"""
|
||||
try:
|
||||
# 读取现有记录
|
||||
metrics = []
|
||||
if self.metrics_file.exists():
|
||||
with open(self.metrics_file, 'r', encoding='utf-8') as f:
|
||||
metrics = json.load(f)
|
||||
|
||||
# 添加新记录
|
||||
metrics.append(asdict(metric))
|
||||
|
||||
# 只保留最近 100 条记录
|
||||
if len(metrics) > 100:
|
||||
metrics = metrics[-100:]
|
||||
|
||||
# 保存
|
||||
with open(self.metrics_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(metrics, f, ensure_ascii=False, indent=2)
|
||||
except Exception as e:
|
||||
print(f"保存配置度量失败: {e}")
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""获取统计信息"""
|
||||
try:
|
||||
if not self.metrics_file.exists():
|
||||
return {
|
||||
"total_config_changes": 0,
|
||||
"first_call_success_rate": 0.0,
|
||||
"avg_retry_count": 0.0,
|
||||
"connection_test_success_rate": 0.0
|
||||
}
|
||||
|
||||
with open(self.metrics_file, 'r', encoding='utf-8') as f:
|
||||
metrics = json.load(f)
|
||||
|
||||
if not metrics:
|
||||
return {
|
||||
"total_config_changes": 0,
|
||||
"first_call_success_rate": 0.0,
|
||||
"avg_retry_count": 0.0,
|
||||
"connection_test_success_rate": 0.0
|
||||
}
|
||||
|
||||
total = len(metrics)
|
||||
success_count = sum(1 for m in metrics if m.get('first_call_success'))
|
||||
total_retries = sum(m.get('retry_count', 0) for m in metrics)
|
||||
connection_test_success = sum(1 for m in metrics if m.get('connection_test_success'))
|
||||
|
||||
return {
|
||||
"total_config_changes": total,
|
||||
"first_call_success_rate": success_count / total if total > 0 else 0.0,
|
||||
"avg_retry_count": total_retries / total if total > 0 else 0.0,
|
||||
"connection_test_success_rate": connection_test_success / total if total > 0 else 0.0,
|
||||
"recent_metrics": metrics[-10:] # 最近 10 条记录
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"获取配置度量统计失败: {e}")
|
||||
return {
|
||||
"total_config_changes": 0,
|
||||
"first_call_success_rate": 0.0,
|
||||
"avg_retry_count": 0.0,
|
||||
"connection_test_success_rate": 0.0
|
||||
}
|
||||
|
||||
|
||||
# 全局单例
|
||||
_metrics_manager: Optional[ConfigMetricsManager] = None
|
||||
|
||||
|
||||
def get_config_metrics(workspace: Path) -> ConfigMetricsManager:
|
||||
"""获取配置度量管理器单例"""
|
||||
global _metrics_manager
|
||||
if _metrics_manager is None:
|
||||
metrics_file = workspace / ".metrics" / "config_metrics.json"
|
||||
_metrics_manager = ConfigMetricsManager(metrics_file)
|
||||
return _metrics_manager
|
||||
|
||||
@@ -444,47 +444,93 @@ REQUIREMENT_CHECK_SYSTEM = """你是一个需求完整性检查器。判断用
|
||||
2. 明确的操作动作(做什么处理)
|
||||
3. 关键参数已指定或有合理默认值
|
||||
|
||||
【关键信息分类】
|
||||
- critical_fields: 缺失后无法执行的关键信息(如:水印类型、目标格式、分类依据)
|
||||
- missing_info: 所有缺失的信息(包括可以使用默认值的)
|
||||
|
||||
【严重程度判断】
|
||||
1. 关键信息缺失(is_complete=false, confidence<0.5):
|
||||
- 缺少操作类型(如:不知道是文字水印还是图片水印)
|
||||
- 缺少必需参数(如:转换格式未指定、分类依据不明)
|
||||
- 存在多种理解方式且无法确定
|
||||
|
||||
2. 一般信息缺失(is_complete=false, confidence=0.5-0.7):
|
||||
- 缺少次要参数但有合理默认值(如:透明度、字体大小)
|
||||
- 描述不够精确但可以推断(如:"整理文件"可推断为按类型分类)
|
||||
|
||||
3. 信息完整但置信度低(is_complete=true, confidence<0.7):
|
||||
- 所有关键信息都有,但描述模糊
|
||||
- 可能存在理解偏差
|
||||
|
||||
4. 信息完整且置信度高(is_complete=true, confidence>=0.7):
|
||||
- 所有关键信息明确
|
||||
- 描述清晰无歧义
|
||||
|
||||
【输出格式】
|
||||
{
|
||||
"is_complete": true或false,
|
||||
"confidence": 0.0到1.0,
|
||||
"reason": "判断理由",
|
||||
"critical_fields": ["关键缺失字段1", "关键缺失字段2"], // 仅当存在关键信息缺失时
|
||||
"missing_info": ["所有缺失信息"],
|
||||
"suggested_defaults": {
|
||||
"参数名": "建议的默认值"
|
||||
}
|
||||
}
|
||||
|
||||
【示例】
|
||||
【示例1 - 关键信息缺失】
|
||||
输入:"给图片加水印"
|
||||
输出:
|
||||
{
|
||||
"is_complete": false,
|
||||
"confidence": 0.3,
|
||||
"reason": "缺少水印类型、内容、位置等关键信息,无法确定用户意图",
|
||||
"critical_fields": ["水印类型", "水印内容"],
|
||||
"missing_info": ["水印类型", "水印内容", "水印位置", "透明度"],
|
||||
"suggested_defaults": {}
|
||||
}
|
||||
|
||||
【示例2 - 一般信息缺失】
|
||||
输入:"给图片加文字水印,内容是'版权所有'"
|
||||
输出:
|
||||
{
|
||||
"is_complete": false,
|
||||
"confidence": 0.6,
|
||||
"reason": "水印类型和内容已明确,但缺少位置信息",
|
||||
"critical_fields": [],
|
||||
"missing_info": ["水印位置", "透明度", "字体大小"],
|
||||
"suggested_defaults": {
|
||||
"position": "右下角",
|
||||
"opacity": 50,
|
||||
"font_size": 24
|
||||
}
|
||||
}
|
||||
|
||||
【示例3 - 信息完整】
|
||||
输入:"把图片转成jpg"
|
||||
输出:
|
||||
{
|
||||
"is_complete": true,
|
||||
"confidence": 0.8,
|
||||
"reason": "目标格式明确,质量可使用默认值85%",
|
||||
"critical_fields": [],
|
||||
"missing_info": [],
|
||||
"suggested_defaults": {
|
||||
"quality": 85
|
||||
}
|
||||
}
|
||||
|
||||
输入:"给图片加水印"
|
||||
输出:
|
||||
{
|
||||
"is_complete": false,
|
||||
"confidence": 0.3,
|
||||
"reason": "缺少水印类型、内容、位置等关键信息",
|
||||
"suggested_defaults": {}
|
||||
}
|
||||
|
||||
输入:"给图片右下角加上'版权所有'的文字水印"
|
||||
【示例4 - 信息完整且详细】
|
||||
输入:"给图片右下角加上'版权所有'的白色文字水印,透明度50%"
|
||||
输出:
|
||||
{
|
||||
"is_complete": true,
|
||||
"confidence": 0.9,
|
||||
"reason": "水印类型、内容、位置都已明确,其他参数可用默认值",
|
||||
"confidence": 0.95,
|
||||
"reason": "水印类型、内容、位置、颜色、透明度都已明确",
|
||||
"critical_fields": [],
|
||||
"missing_info": [],
|
||||
"suggested_defaults": {
|
||||
"opacity": 50,
|
||||
"font_size": 24,
|
||||
"color": "white"
|
||||
"font_size": 24
|
||||
}
|
||||
}"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user