mirror of
https://github.com/jihe520/MathModelAgent.git
synced 2026-10-02 02:44:56 +08:00
refactor: 重构 LLM Provider 模式、WebSocket 重连、图表字体跨平台兼容 (#70)
LLM 层重构: - 将 LLM 调用层重构为 Provider 模式,支持多 API 类型 - 新增 Anthropic、OpenAI Chat、OpenAI Responses 三种 Provider - 新增 LLM 类型定义和工厂类 WebSocket 重连: - 添加指数退避自动重连机制(初始 1s,最大 30s,最多 10 次) - 区分手动关闭与意外断开,仅意外断开触发重连 - 在任务页面顶部添加连接状态指示灯(绿/黄/红) 图表字体跨平台兼容: - 在 backend/fonts/ 存放 SimHei 字体文件 - create_work_dir() 自动复制字体到任务工作目录 - 两个解释器从工作目录动态加载字体 - E2B 移除 apt-get install 依赖,改用上传字体文件 - 统一三处字体优先级配置 其他: - 重构注释规范、修复类型错误、添加 lint hook - 更新 CLAUDE.md 项目文档
This commit is contained in:
@@ -50,7 +50,9 @@ case "$file" in
|
||||
;;
|
||||
frontend/src/*.vue|frontend/src/*.ts|frontend/src/*/*.vue|frontend/src/*/*.ts|frontend/src/*/*/*.vue|frontend/src/*/*/*.ts|frontend/src/*/*/*/*.vue|frontend/src/*/*/*/*.ts)
|
||||
cd "$REPO_ROOT/frontend" || exit 0
|
||||
run_check npx biome check "$file"
|
||||
# 去掉 frontend/ 前缀,因为已经 cd 到 frontend 目录
|
||||
rel_file="${file#frontend/}"
|
||||
run_check npx biome check "$rel_file"
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# 使用官方推荐的 Python 基础镜像
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
# 安装中文字体(确保 matplotlib 图表中文正常显示)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends fonts-noto-cjk && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 安装 uv(推荐直接用官方 distroless 镜像复制二进制文件,速度快且干净)
|
||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
||||
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
"""应用配置模块,基于 pydantic-settings 管理环境变量和全局配置。"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BeforeValidator
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
import os
|
||||
from typing import Annotated, Optional
|
||||
|
||||
|
||||
class ApiType(str, Enum):
|
||||
"""LLM API 类型。"""
|
||||
OPENAI_CHAT = "openai-chat"
|
||||
OPENAI_RESPONSES = "openai-responses"
|
||||
ANTHROPIC = "anthropic"
|
||||
|
||||
|
||||
def parse_cors(value: str) -> list[str]:
|
||||
"""将 CORS 配置字符串解析为 URL 列表。
|
||||
|
||||
@@ -24,63 +33,38 @@ def parse_cors(value: str) -> list[str]:
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""全局应用配置,从环境变量和 .env 文件加载。"""
|
||||
|
||||
ENV: str = "dev"
|
||||
|
||||
COORDINATOR_API_TYPE: Optional[ApiType] = None
|
||||
COORDINATOR_API_KEY: Optional[str] = None
|
||||
COORDINATOR_MODEL: Optional[str] = None
|
||||
COORDINATOR_BASE_URL: Optional[str] = None
|
||||
COORDINATOR_MAX_TOKENS: Optional[int] = None
|
||||
COORDINATOR_CONTEXT_WINDOW: int = 128000
|
||||
|
||||
MODELER_API_TYPE: Optional[ApiType] = None
|
||||
MODELER_API_KEY: Optional[str] = None
|
||||
MODELER_MODEL: Optional[str] = None
|
||||
MODELER_BASE_URL: Optional[str] = None
|
||||
MODELER_MAX_TOKENS: Optional[int] = None
|
||||
MODELER_CONTEXT_WINDOW: int = 128000
|
||||
|
||||
CODER_API_TYPE: Optional[ApiType] = None
|
||||
CODER_API_KEY: Optional[str] = None
|
||||
CODER_MODEL: Optional[str] = None
|
||||
CODER_BASE_URL: Optional[str] = None
|
||||
CODER_MAX_TOKENS: Optional[int] = None
|
||||
CODER_CONTEXT_WINDOW: int = 128000
|
||||
|
||||
WRITER_API_TYPE: Optional[ApiType] = None
|
||||
WRITER_API_KEY: Optional[str] = None
|
||||
WRITER_MODEL: Optional[str] = None
|
||||
WRITER_BASE_URL: Optional[str] = None
|
||||
WRITER_MAX_TOKENS: Optional[int] = None
|
||||
|
||||
# Fallback LLM 配置,用于主 LLM 失败时的 Hand Off
|
||||
FALLBACK_COORDINATOR_API_KEY: Optional[str] = None
|
||||
FALLBACK_COORDINATOR_MODEL: Optional[str] = None
|
||||
FALLBACK_COORDINATOR_BASE_URL: Optional[str] = None
|
||||
FALLBACK_COORDINATOR_MAX_TOKENS: Optional[int] = None
|
||||
|
||||
FALLBACK_MODELER_API_KEY: Optional[str] = None
|
||||
FALLBACK_MODELER_MODEL: Optional[str] = None
|
||||
FALLBACK_MODELER_BASE_URL: Optional[str] = None
|
||||
FALLBACK_MODELER_MAX_TOKENS: Optional[int] = None
|
||||
|
||||
FALLBACK_CODER_API_KEY: Optional[str] = None
|
||||
FALLBACK_CODER_MODEL: Optional[str] = None
|
||||
FALLBACK_CODER_BASE_URL: Optional[str] = None
|
||||
FALLBACK_CODER_MAX_TOKENS: Optional[int] = None
|
||||
|
||||
FALLBACK_WRITER_API_KEY: Optional[str] = None
|
||||
FALLBACK_WRITER_MODEL: Optional[str] = None
|
||||
FALLBACK_WRITER_BASE_URL: Optional[str] = None
|
||||
FALLBACK_WRITER_MAX_TOKENS: Optional[int] = None
|
||||
|
||||
# 评估器配置(独立的便宜模型,不复用 writer LLM)
|
||||
EVALUATOR_API_KEY: Optional[str] = None
|
||||
EVALUATOR_MODEL: Optional[str] = None
|
||||
EVALUATOR_BASE_URL: Optional[str] = None
|
||||
|
||||
# Feedback Rerun 配置
|
||||
MAX_FEEDBACK_ROUNDS: int = 2
|
||||
EVALUATION_THRESHOLD: float = 0.6
|
||||
WRITER_CONTEXT_WINDOW: int = 128000
|
||||
|
||||
MAX_CHAT_TURNS: Optional[int] = None
|
||||
MAX_RETRIES: Optional[int] = None
|
||||
MAX_COORDINATOR_RETRIES: int = 5
|
||||
MAX_MODELER_RETRIES: int = 5
|
||||
E2B_API_KEY: Optional[str] = None
|
||||
LOG_LEVEL: str = "DEBUG"
|
||||
DEBUG: bool = True
|
||||
|
||||
@@ -3,27 +3,41 @@
|
||||
from typing import Any
|
||||
from app.core.llm.llm import LLM, simple_chat
|
||||
from app.utils.log_util import logger
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
|
||||
# TODO: Memory 的管理
|
||||
# TODO: 评估任务完成情况,rethinking
|
||||
|
||||
# 每个字符估算的 token 数(中英混合文本的保守估计)
|
||||
_CHARS_PER_TOKEN = 3
|
||||
# 触发压缩的 token 占比阈值(相对 context_window)
|
||||
_DEFAULT_TOKEN_THRESHOLD_RATIO = 0.75
|
||||
|
||||
|
||||
class Agent:
|
||||
"""Agent 基类,管理对话历史、轮次控制和记忆压缩。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
model: LLM,
|
||||
max_chat_turns: int | None = None, # 单个agent最大对话轮次,None表示无限制
|
||||
max_memory: int = 12, # 最大记忆轮次
|
||||
context_window: int = 128000, # 模型上下文窗口大小(token)
|
||||
token_threshold_ratio: float = _DEFAULT_TOKEN_THRESHOLD_RATIO,
|
||||
) -> None:
|
||||
self.task_id = task_id
|
||||
self.model = model
|
||||
self.chat_history: list[dict] = [] # 存储对话历史
|
||||
self.max_chat_turns = max_chat_turns # 最大对话轮次
|
||||
self.current_chat_turns = 0 # 当前对话轮次计数器
|
||||
self.max_memory = max_memory # 最大记忆轮次
|
||||
self.context_window = context_window
|
||||
self.token_threshold_ratio = token_threshold_ratio
|
||||
self.current_token_count = 0 # 当前历史的估算 token 数
|
||||
|
||||
def _estimate_tokens(self, text: str) -> int:
|
||||
"""估算文本的 token 数量。"""
|
||||
return max(1, len(text) // _CHARS_PER_TOKEN)
|
||||
|
||||
def _estimate_message_tokens(self, msg: dict) -> int:
|
||||
"""估算单条消息的 token 数(含结构开销)。"""
|
||||
content = msg.get("content") or ""
|
||||
# 4 token 额外开销(role、分隔符等)
|
||||
return self._estimate_tokens(content) + 4
|
||||
|
||||
async def run(self, prompt: str, system_prompt: str, sub_title: str) -> Any:
|
||||
"""执行 Agent 对话并返回模型响应。
|
||||
@@ -38,7 +52,6 @@ class Agent:
|
||||
"""
|
||||
try:
|
||||
logger.info(f"{self.__class__.__name__}:开始:执行对话")
|
||||
self.current_chat_turns = 0 # 重置对话轮次计数器
|
||||
|
||||
# 更新对话历史
|
||||
await self.append_chat_history({"role": "system", "content": system_prompt})
|
||||
@@ -50,8 +63,17 @@ class Agent:
|
||||
agent_name=self.__class__.__name__,
|
||||
sub_title=sub_title,
|
||||
)
|
||||
response_content = response.choices[0].message.content
|
||||
response_content = response.content
|
||||
self.chat_history.append({"role": "assistant", "content": response_content})
|
||||
|
||||
# 使用 API 返回的实际 prompt_tokens 更新计数
|
||||
if response.usage.prompt_tokens > 0:
|
||||
self.current_token_count = response.usage.prompt_tokens
|
||||
else:
|
||||
self.current_token_count += self._estimate_message_tokens(
|
||||
{"content": response_content}
|
||||
)
|
||||
|
||||
logger.info(f"{self.__class__.__name__}:完成:执行对话")
|
||||
return response_content
|
||||
except Exception as e:
|
||||
@@ -60,33 +82,27 @@ class Agent:
|
||||
return error_msg
|
||||
|
||||
async def append_chat_history(self, msg: dict) -> None:
|
||||
"""向对话历史追加消息,并在必要时触发记忆清理。
|
||||
"""向对话历史追加消息,并在必要时触发记忆压缩。
|
||||
|
||||
Args:
|
||||
msg: 消息字典,需包含 role 和 content 字段。
|
||||
"""
|
||||
ic(f"添加消息: role={msg.get('role')}, 当前历史长度={len(self.chat_history)}")
|
||||
self.chat_history.append(msg)
|
||||
ic(f"添加后历史长度={len(self.chat_history)}")
|
||||
self.current_token_count += self._estimate_message_tokens(msg)
|
||||
|
||||
# 只有在添加非tool消息时才进行内存清理,避免在工具调用期间破坏消息结构
|
||||
if msg.get("role") != "tool":
|
||||
ic("触发内存清理")
|
||||
await self.clear_memory()
|
||||
else:
|
||||
ic("跳过内存清理(tool消息)")
|
||||
await self.compress_if_needed()
|
||||
|
||||
async def clear_memory(self):
|
||||
"""当聊天历史超过最大记忆轮次时,使用 simple_chat 进行总结压缩。"""
|
||||
ic(f"检查内存清理: 当前={len(self.chat_history)}, 最大={self.max_memory}")
|
||||
|
||||
if len(self.chat_history) <= self.max_memory:
|
||||
ic("无需清理内存")
|
||||
async def compress_if_needed(self) -> None:
|
||||
"""当 token 数超过上下文窗口阈值时,使用 LLM 总结压缩历史。"""
|
||||
threshold = int(self.context_window * self.token_threshold_ratio)
|
||||
if self.current_token_count <= threshold:
|
||||
return
|
||||
|
||||
ic("开始内存清理")
|
||||
logger.info(
|
||||
f"{self.__class__.__name__}:开始清除记忆,当前记录数:{len(self.chat_history)}"
|
||||
f"{self.__class__.__name__}:触发记忆压缩,"
|
||||
f"当前 token ~{self.current_token_count},阈值 {threshold}"
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -99,12 +115,10 @@ class Agent:
|
||||
|
||||
# 查找需要保留的消息范围 - 保留最后几条完整的对话和工具调用
|
||||
preserve_start_idx = self._find_safe_preserve_point()
|
||||
ic(f"保留起始索引: {preserve_start_idx}")
|
||||
|
||||
# 确定需要总结的消息范围
|
||||
start_idx = 1 if system_msg else 0
|
||||
end_idx = preserve_start_idx
|
||||
ic(f"总结范围: {start_idx} -> {end_idx}")
|
||||
|
||||
if end_idx > start_idx:
|
||||
# 构造总结提示
|
||||
@@ -135,59 +149,54 @@ class Agent:
|
||||
new_history.extend(self.chat_history[preserve_start_idx:])
|
||||
|
||||
self.chat_history = new_history
|
||||
ic(f"内存清理完成,新历史长度: {len(self.chat_history)}")
|
||||
|
||||
# 重新估算 token 数
|
||||
self.current_token_count = sum(
|
||||
self._estimate_message_tokens(m) for m in self.chat_history
|
||||
)
|
||||
logger.info(
|
||||
f"{self.__class__.__name__}:记忆清除完成,压缩至:{len(self.chat_history)}条记录"
|
||||
f"{self.__class__.__name__}:记忆压缩完成,"
|
||||
f"压缩至 {len(self.chat_history)} 条记录,"
|
||||
f"约 {self.current_token_count} tokens"
|
||||
)
|
||||
else:
|
||||
logger.info(f"{self.__class__.__name__}:无需清除记忆,记录数量合理")
|
||||
logger.info(f"{self.__class__.__name__}:无需压缩,记录数量合理")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"记忆清除失败,使用简单切片策略: {str(e)}")
|
||||
logger.error(f"记忆压缩失败,使用简单切片策略: {str(e)}")
|
||||
# 如果总结失败,回退到安全的策略:保留系统消息和最后几条消息,确保工具调用完整性
|
||||
safe_history = self._get_safe_fallback_history()
|
||||
self.chat_history = safe_history
|
||||
self.current_token_count = sum(
|
||||
self._estimate_message_tokens(m) for m in self.chat_history
|
||||
)
|
||||
|
||||
def _find_safe_preserve_point(self) -> int:
|
||||
"""找到安全的保留起始点,确保不会破坏工具调用序列"""
|
||||
"""找到安全的保留起始点,确保不会破坏工具调用序列。"""
|
||||
# 最少保留最后3条消息,确保基本对话完整性
|
||||
min_preserve = min(3, len(self.chat_history))
|
||||
preserve_start = len(self.chat_history) - min_preserve
|
||||
ic(
|
||||
f"寻找安全保留点: 历史长度={len(self.chat_history)}, 最少保留={min_preserve}, 开始位置={preserve_start}"
|
||||
)
|
||||
|
||||
# 从后往前查找,确保不会在工具调用序列中间切断
|
||||
for i in range(preserve_start, -1, -1):
|
||||
if i >= len(self.chat_history):
|
||||
continue
|
||||
|
||||
# 检查从这个位置开始是否是安全的(没有孤立的tool消息)
|
||||
is_safe = self._is_safe_cut_point(i)
|
||||
ic(f"检查位置 {i}: 安全={is_safe}")
|
||||
if is_safe:
|
||||
ic(f"找到安全保留点: {i}")
|
||||
if self._is_safe_cut_point(i):
|
||||
return i
|
||||
|
||||
# 如果找不到安全点,至少保留最后1条消息
|
||||
fallback = len(self.chat_history) - 1
|
||||
ic(f"未找到安全点,使用备用位置: {fallback}")
|
||||
return fallback
|
||||
return len(self.chat_history) - 1
|
||||
|
||||
def _is_safe_cut_point(self, start_idx: int) -> bool:
|
||||
"""检查从指定位置开始切割是否安全(不会产生孤立的tool消息)"""
|
||||
"""检查从指定位置开始切割是否安全(不会产生孤立的tool消息)。"""
|
||||
if start_idx >= len(self.chat_history):
|
||||
ic(f"切割点 {start_idx} >= 历史长度,安全")
|
||||
return True
|
||||
|
||||
# 检查切割后的消息序列是否有孤立的tool消息
|
||||
tool_messages = []
|
||||
for i in range(start_idx, len(self.chat_history)):
|
||||
msg = self.chat_history[i]
|
||||
if isinstance(msg, dict) and msg.get("role") == "tool":
|
||||
tool_call_id = msg.get("tool_call_id")
|
||||
tool_messages.append((i, tool_call_id))
|
||||
ic(f"发现tool消息在位置 {i}, tool_call_id={tool_call_id}")
|
||||
|
||||
# 向前查找对应的tool_calls消息
|
||||
if tool_call_id:
|
||||
@@ -202,22 +211,17 @@ class Agent:
|
||||
for tool_call in prev_msg["tool_calls"]:
|
||||
if tool_call.get("id") == tool_call_id:
|
||||
found_tool_call = True
|
||||
ic(f"找到对应的tool_call在位置 {j}")
|
||||
break
|
||||
if found_tool_call:
|
||||
break
|
||||
|
||||
if not found_tool_call:
|
||||
ic(
|
||||
f"❌ tool消息 {tool_call_id} 没有找到对应的tool_call,切割点不安全"
|
||||
)
|
||||
return False
|
||||
|
||||
ic(f"切割点 {start_idx} 安全,检查了 {len(tool_messages)} 个tool消息")
|
||||
return True
|
||||
|
||||
def _get_safe_fallback_history(self) -> list:
|
||||
"""获取安全的后备历史记录,确保不会有孤立的tool消息"""
|
||||
"""获取安全的后备历史记录,确保不会有孤立的tool消息。"""
|
||||
if not self.chat_history:
|
||||
return []
|
||||
|
||||
@@ -242,54 +246,13 @@ class Agent:
|
||||
|
||||
return safe_history
|
||||
|
||||
def _find_last_unmatched_tool_call(self) -> int | None:
|
||||
"""查找最后一个未匹配的tool call的索引"""
|
||||
ic("开始查找未匹配的tool_call")
|
||||
|
||||
# 从后往前查找,寻找没有对应tool response的tool call
|
||||
for i in range(len(self.chat_history) - 1, -1, -1):
|
||||
msg = self.chat_history[i]
|
||||
|
||||
# 检查是否是包含tool_calls的消息
|
||||
if isinstance(msg, dict) and "tool_calls" in msg and msg["tool_calls"]:
|
||||
ic(f"在位置 {i} 发现tool_calls消息")
|
||||
|
||||
# 检查每个tool call是否都有对应的response
|
||||
for tool_call in msg["tool_calls"]:
|
||||
tool_call_id = tool_call.get("id")
|
||||
ic(f"检查tool_call_id: {tool_call_id}")
|
||||
|
||||
if tool_call_id:
|
||||
# 在后续消息中查找对应的tool response
|
||||
response_found = False
|
||||
for j in range(i + 1, len(self.chat_history)):
|
||||
response_msg = self.chat_history[j]
|
||||
if (
|
||||
isinstance(response_msg, dict)
|
||||
and response_msg.get("role") == "tool"
|
||||
and response_msg.get("tool_call_id") == tool_call_id
|
||||
):
|
||||
ic(f"找到匹配的tool响应在位置 {j}")
|
||||
response_found = True
|
||||
break
|
||||
|
||||
if not response_found:
|
||||
# 找到未匹配的tool call
|
||||
ic(f"❌ 发现未匹配的tool_call在位置 {i}, id={tool_call_id}")
|
||||
return i
|
||||
|
||||
ic("没有发现未匹配的tool_call")
|
||||
return None
|
||||
|
||||
def _format_history_for_summary(self, history: list[dict]) -> str:
|
||||
"""格式化历史记录用于总结"""
|
||||
"""格式化历史记录用于总结。"""
|
||||
formatted = []
|
||||
for msg in history:
|
||||
role = msg["role"]
|
||||
content = (
|
||||
msg["content"][:500] + "..."
|
||||
if len(msg["content"]) > 500
|
||||
else msg["content"]
|
||||
) # 限制长度
|
||||
content = msg.get("content") or ""
|
||||
if len(content) > 500:
|
||||
content = content[:500] + "..."
|
||||
formatted.append(f"{role}: {content}")
|
||||
return "\n".join(formatted)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""代码手 Agent 模块,负责生成和执行 Python 代码完成建模任务。"""
|
||||
|
||||
from app.core.agents.agent import Agent
|
||||
from app.config.setting import settings
|
||||
from app.config.setting import settings, ApiType
|
||||
from app.utils.log_util import logger
|
||||
from app.services.redis_manager import redis_manager
|
||||
from app.schemas.response import SystemMessage, InterpreterMessage
|
||||
@@ -12,8 +12,7 @@ from app.core.prompts import CODER_PROMPT
|
||||
from app.utils.common_utils import get_current_files
|
||||
import json
|
||||
from app.core.prompts import get_reflection_prompt
|
||||
from app.core.functions import coder_tools
|
||||
from app.tools.tool_registry import tool_registry
|
||||
from app.core.functions import coder_tools, coder_tools_anthropic
|
||||
|
||||
# TODO: 时间等待过久,stop 进程
|
||||
# TODO: 支持 cuda
|
||||
@@ -22,24 +21,24 @@ from app.tools.tool_registry import tool_registry
|
||||
|
||||
class CoderAgent(Agent):
|
||||
"""代码手 Agent,通过 LLM 生成代码并在解释器中执行,支持错误反思和重试。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
model: LLM,
|
||||
work_dir: str, # 工作目录
|
||||
max_chat_turns: int
|
||||
| None = settings.MAX_CHAT_TURNS, # 最大聊天次数,None表示无限制
|
||||
max_chat_turns: int | None = settings.MAX_CHAT_TURNS, # 最大聊天次数,None表示无限制
|
||||
max_retries: int | None = settings.MAX_RETRIES, # 最大反思次数,None表示无限制
|
||||
code_interpreter: BaseCodeInterpreter | None = None,
|
||||
context_window: int = 128000,
|
||||
) -> None:
|
||||
super().__init__(task_id, model, max_chat_turns)
|
||||
super().__init__(task_id, model, context_window)
|
||||
self.work_dir = work_dir
|
||||
self.max_chat_turns = max_chat_turns
|
||||
self.current_chat_turns = 0
|
||||
self.max_retries = max_retries
|
||||
self.is_first_run = True
|
||||
self.system_prompt = CODER_PROMPT
|
||||
self.code_interpreter = code_interpreter
|
||||
self.available_tools = coder_tools # 可被 workflow 动态更新
|
||||
|
||||
async def run(self, prompt: str, subtask_title: str) -> CoderToWriter: # type: ignore[reportIncompatibleMethodOverride]
|
||||
"""执行代码手子任务,生成并运行代码。
|
||||
@@ -55,6 +54,10 @@ class CoderAgent(Agent):
|
||||
assert self.code_interpreter is not None, "code_interpreter 未初始化"
|
||||
self.code_interpreter.add_section(subtask_title)
|
||||
|
||||
# 根据 api_type 选择 tools 格式
|
||||
api_type = self.model.api_type
|
||||
tools = coder_tools_anthropic if api_type == ApiType.ANTHROPIC else coder_tools
|
||||
|
||||
# 如果是第一次运行,则添加系统提示
|
||||
if self.is_first_run:
|
||||
logger.info("首次运行,添加系统提示和数据集文件信息")
|
||||
@@ -84,18 +87,13 @@ class CoderAgent(Agent):
|
||||
self.task_id,
|
||||
SystemMessage(content="超过最大尝试次数", type="error"),
|
||||
)
|
||||
logger.warning(
|
||||
f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}"
|
||||
)
|
||||
logger.warning(f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}")
|
||||
return CoderToWriter(
|
||||
code_response=f"任务失败,超过最大尝试次数{self.max_retries}, 最后错误信息: {last_error_message}",
|
||||
created_images=[],
|
||||
)
|
||||
created_images=[])
|
||||
|
||||
if (
|
||||
self.max_chat_turns is not None
|
||||
and self.current_chat_turns >= self.max_chat_turns
|
||||
):
|
||||
|
||||
if self.max_chat_turns is not None and self.current_chat_turns >= self.max_chat_turns:
|
||||
logger.error(f"超过最大聊天次数: {self.max_chat_turns}")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
@@ -107,41 +105,31 @@ class CoderAgent(Agent):
|
||||
|
||||
self.current_chat_turns += 1
|
||||
logger.info(f"当前对话轮次: {self.current_chat_turns}")
|
||||
|
||||
|
||||
try:
|
||||
response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
tools=self.available_tools,
|
||||
tools=tools,
|
||||
tool_choice="auto",
|
||||
agent_name=self.__class__.__name__,
|
||||
)
|
||||
|
||||
# 如果有工具调用
|
||||
if (
|
||||
hasattr(response.choices[0].message, "tool_calls")
|
||||
and response.choices[0].message.tool_calls
|
||||
):
|
||||
if response.tool_calls:
|
||||
logger.info("检测到工具调用")
|
||||
tool_call = response.choices[0].message.tool_calls[0]
|
||||
tool_call = response.tool_calls[0]
|
||||
tool_id = tool_call.id
|
||||
|
||||
tool_name = tool_call.function.name
|
||||
tool_args = json.loads(tool_call.function.arguments)
|
||||
if tool_call.name == "execute_code":
|
||||
logger.info(f"调用工具: {tool_call.name}")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content=f"代码手调用{tool_call.name}工具"
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(f"调用工具: {tool_name}")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(content=f"代码手调用{tool_name}工具"),
|
||||
)
|
||||
|
||||
# 更新对话历史 - 添加助手的响应
|
||||
await self.append_chat_history(
|
||||
response.choices[0].message.model_dump()
|
||||
)
|
||||
logger.info(response.choices[0].message.model_dump())
|
||||
|
||||
if tool_name == "execute_code":
|
||||
code = tool_args["code"]
|
||||
code = json.loads(tool_call.arguments)["code"]
|
||||
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
@@ -150,8 +138,21 @@ class CoderAgent(Agent):
|
||||
),
|
||||
)
|
||||
|
||||
# 执行代码
|
||||
logger.info("执行代码工具调用")
|
||||
# 更新对话历史 - 添加助手的响应
|
||||
assistant_msg: dict = {"role": "assistant", "content": response.content}
|
||||
if response.tool_calls:
|
||||
assistant_msg["tool_calls"] = [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {"name": tc.name, "arguments": tc.arguments},
|
||||
}
|
||||
for tc in response.tool_calls
|
||||
]
|
||||
await self.append_chat_history(assistant_msg)
|
||||
|
||||
# 执行工具调用
|
||||
logger.info("执行工具调用")
|
||||
(
|
||||
text_to_gpt,
|
||||
error_occurred,
|
||||
@@ -160,6 +161,7 @@ class CoderAgent(Agent):
|
||||
|
||||
# 添加工具执行结果
|
||||
if error_occurred:
|
||||
# 即使发生错误也要添加tool响应
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "tool",
|
||||
@@ -171,19 +173,13 @@ class CoderAgent(Agent):
|
||||
|
||||
logger.warning(f"代码执行错误: {error_message}")
|
||||
retry_count += 1
|
||||
logger.info(
|
||||
f"当前尝试次:{retry_count} / {self.max_retries}"
|
||||
)
|
||||
logger.info(f"当前尝试次:{retry_count} / {self.max_retries}")
|
||||
last_error_message = error_message
|
||||
reflection_prompt = get_reflection_prompt(
|
||||
error_message, code
|
||||
)
|
||||
reflection_prompt = get_reflection_prompt(error_message, code)
|
||||
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content="代码手反思纠正错误", type="error"
|
||||
),
|
||||
SystemMessage(content="代码手反思纠正错误", type="error"),
|
||||
)
|
||||
|
||||
await self.append_chat_history(
|
||||
@@ -191,6 +187,7 @@ class CoderAgent(Agent):
|
||||
)
|
||||
continue
|
||||
else:
|
||||
# 成功执行的tool响应
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "tool",
|
||||
@@ -199,38 +196,21 @@ class CoderAgent(Agent):
|
||||
"content": text_to_gpt,
|
||||
}
|
||||
)
|
||||
# 成功执行后继续循环,等待下一步指令
|
||||
continue
|
||||
else:
|
||||
# 通过 tool_registry 分发其他工具(如 search_web)
|
||||
try:
|
||||
result = await tool_registry.dispatch(
|
||||
tool_name, tool_args, self.task_id
|
||||
)
|
||||
except ValueError as e:
|
||||
result = f"工具调用失败: {e}"
|
||||
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_id,
|
||||
"name": tool_name,
|
||||
"content": result,
|
||||
}
|
||||
)
|
||||
continue
|
||||
else:
|
||||
# 没有工具调用,表示任务完成
|
||||
logger.info("没有工具调用,任务完成")
|
||||
return CoderToWriter(
|
||||
code_response=response.choices[0].message.content,
|
||||
code_response=response.content,
|
||||
created_images=await self.code_interpreter.get_created_images(
|
||||
subtask_title
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"执行过程中发生异常: {str(e)}")
|
||||
retry_count += 1
|
||||
last_error_message = str(e)
|
||||
continue
|
||||
logger.info(f"{self.__class__.__name__}:完成:执行子任务: {subtask_title}")
|
||||
logger.info(f"{self.__class__.__name__}:完成:执行子任务: {subtask_title}")
|
||||
@@ -3,7 +3,6 @@
|
||||
from app.core.agents.agent import Agent
|
||||
from app.core.llm.llm import LLM
|
||||
from app.core.prompts import COORDINATOR_PROMPT
|
||||
from app.config.setting import settings
|
||||
import json
|
||||
import re
|
||||
from app.utils.log_util import logger
|
||||
@@ -16,12 +15,10 @@ class CoordinatorAgent(Agent):
|
||||
self,
|
||||
task_id: str,
|
||||
model: LLM,
|
||||
max_chat_turns: int = 30,
|
||||
max_retries: int = settings.MAX_COORDINATOR_RETRIES,
|
||||
context_window: int = 128000,
|
||||
) -> None:
|
||||
super().__init__(task_id, model, max_chat_turns)
|
||||
super().__init__(task_id, model, context_window)
|
||||
self.system_prompt = COORDINATOR_PROMPT
|
||||
self.max_retries = max_retries
|
||||
|
||||
async def run(self, ques_all: str) -> CoordinatorToModeler: # type: ignore[reportIncompatibleMethodOverride]
|
||||
"""解析用户输入的问题并格式化为结构化 JSON。
|
||||
@@ -31,22 +28,19 @@ class CoordinatorAgent(Agent):
|
||||
|
||||
Returns:
|
||||
CoordinatorToModeler 对象,包含结构化问题和问题数量。
|
||||
|
||||
Raises:
|
||||
ValueError: 超过最大重试次数仍无法解析 JSON 时抛出。
|
||||
"""
|
||||
await self.append_chat_history(
|
||||
{"role": "system", "content": self.system_prompt}
|
||||
)
|
||||
await self.append_chat_history({"role": "user", "content": ques_all})
|
||||
|
||||
for attempt in range(self.max_retries + 1):
|
||||
attempt = 0
|
||||
while True:
|
||||
try:
|
||||
response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
agent_name=self.__class__.__name__,
|
||||
)
|
||||
json_str = response.choices[0].message.content
|
||||
json_str = response.content or ""
|
||||
|
||||
# 清理 JSON 字符串
|
||||
json_str = json_str.replace("```json", "").replace("```", "").strip()
|
||||
@@ -61,7 +55,8 @@ class CoordinatorAgent(Agent):
|
||||
return CoordinatorToModeler(questions=questions, ques_count=ques_count)
|
||||
|
||||
except (json.JSONDecodeError, ValueError, KeyError) as e:
|
||||
logger.warning(f"解析失败 (尝试 {attempt + 1}/{self.max_retries + 1}): {str(e)}")
|
||||
attempt += 1
|
||||
logger.warning(f"解析失败 (尝试 {attempt}): {str(e)}")
|
||||
|
||||
# 添加错误反馈提示
|
||||
error_prompt = f"⚠️ 上次响应格式错误: {str(e)}。请严格输出JSON格式"
|
||||
@@ -69,7 +64,3 @@ class CoordinatorAgent(Agent):
|
||||
"role": "system",
|
||||
"content": self.system_prompt + "\n" + error_prompt
|
||||
})
|
||||
|
||||
raise ValueError(
|
||||
f"CoordinatorAgent 超过最大重试次数({self.max_retries}),无法解析 JSON 响应"
|
||||
)
|
||||
|
||||
@@ -3,10 +3,7 @@
|
||||
from app.core.agents.agent import Agent
|
||||
from app.core.llm.llm import LLM
|
||||
from app.core.prompts import MODELER_PROMPT
|
||||
from app.config.setting import settings
|
||||
from app.schemas.A2A import CoordinatorToModeler, ModelerToCoder
|
||||
from app.services.redis_manager import redis_manager
|
||||
from app.schemas.response import SystemMessage
|
||||
from app.utils.log_util import logger
|
||||
import json
|
||||
import re
|
||||
@@ -56,17 +53,14 @@ def repair_json(json_str: str) -> dict | None:
|
||||
|
||||
class ModelerAgent(Agent):
|
||||
"""建模手 Agent,分析问题类型并制定建模方案、求解方法和可视化策略。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
task_id: str,
|
||||
model: LLM,
|
||||
max_chat_turns: int = 30,
|
||||
max_retries: int = settings.MAX_MODELER_RETRIES,
|
||||
context_window: int = 128000,
|
||||
) -> None:
|
||||
super().__init__(task_id, model, max_chat_turns)
|
||||
super().__init__(task_id, model, context_window)
|
||||
self.system_prompt = MODELER_PROMPT
|
||||
self.max_retries = max_retries
|
||||
|
||||
async def run(self, coordinator_to_modeler: CoordinatorToModeler) -> ModelerToCoder: # type: ignore[reportIncompatibleMethodOverride]
|
||||
"""根据协调者拆解的问题生成建模方案。
|
||||
@@ -76,9 +70,6 @@ class ModelerAgent(Agent):
|
||||
|
||||
Returns:
|
||||
ModelerToCoder 对象,包含各问题的建模解决方案。
|
||||
|
||||
Raises:
|
||||
ValueError: 超过最大重试次数仍无法解析 JSON 时抛出。
|
||||
"""
|
||||
await self.append_chat_history(
|
||||
{"role": "system", "content": self.system_prompt}
|
||||
@@ -90,13 +81,14 @@ class ModelerAgent(Agent):
|
||||
}
|
||||
)
|
||||
|
||||
for attempt in range(self.max_retries + 1):
|
||||
attempt = 0
|
||||
while True:
|
||||
response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
agent_name=self.__class__.__name__,
|
||||
)
|
||||
|
||||
json_str = response.choices[0].message.content
|
||||
json_str = response.content
|
||||
if not json_str:
|
||||
raise ValueError("返回的 JSON 字符串为空,请检查输入内容。")
|
||||
|
||||
@@ -105,119 +97,16 @@ class ModelerAgent(Agent):
|
||||
ic(questions_solution)
|
||||
return ModelerToCoder(questions_solution=questions_solution)
|
||||
|
||||
attempt += 1
|
||||
logger.warning(
|
||||
f"JSON 解析失败 (第{attempt + 1}/{self.max_retries + 1}次),请求模型重新生成"
|
||||
f"JSON 解析失败 (第{attempt}次),请求模型重新生成"
|
||||
)
|
||||
await self.append_chat_history(
|
||||
{"role": "assistant", "content": json_str}
|
||||
)
|
||||
await self.append_chat_history({"role": "assistant", "content": json_str})
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "user",
|
||||
"content": '你返回的JSON格式有误,请严格按照JSON格式重新输出,注意字符串值内的双引号必须转义为\\",不要包含未转义的特殊字符。',
|
||||
"content": "你返回的JSON格式有误,请严格按照JSON格式重新输出,注意字符串值内的双引号必须转义为\\\",不要包含未转义的特殊字符。",
|
||||
}
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"ModelerAgent 超过最大重试次数({self.max_retries}),无法解析 JSON 响应"
|
||||
)
|
||||
|
||||
async def run_with_tools(
|
||||
self,
|
||||
coordinator_to_modeler: CoordinatorToModeler,
|
||||
tools: list[dict] | None = None,
|
||||
) -> ModelerToCoder:
|
||||
"""支持工具调用的建模方案生成(单次 tool call 模式)。
|
||||
|
||||
Args:
|
||||
coordinator_to_modeler: 协调者传递的结构化问题信息。
|
||||
tools: OpenAI function-calling 格式的工具列表。
|
||||
|
||||
Returns:
|
||||
ModelerToCoder 对象,包含各问题的建模解决方案。
|
||||
|
||||
Raises:
|
||||
ValueError: 超过最大重试次数仍无法解析 JSON 时抛出。
|
||||
"""
|
||||
from app.tools.tool_registry import tool_registry
|
||||
|
||||
await self.append_chat_history(
|
||||
{"role": "system", "content": self.system_prompt}
|
||||
)
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "user",
|
||||
"content": json.dumps(coordinator_to_modeler.questions),
|
||||
}
|
||||
)
|
||||
|
||||
for attempt in range(self.max_retries + 1):
|
||||
response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
tools=tools,
|
||||
tool_choice="auto" if tools else None,
|
||||
agent_name=self.__class__.__name__,
|
||||
)
|
||||
|
||||
msg = response.choices[0].message
|
||||
|
||||
# 处理工具调用(单次,类似 WriterAgent)
|
||||
if hasattr(msg, "tool_calls") and msg.tool_calls:
|
||||
tool_call = msg.tool_calls[0]
|
||||
tool_id = tool_call.id
|
||||
tool_name = tool_call.function.name
|
||||
tool_args = json.loads(tool_call.function.arguments)
|
||||
|
||||
logger.info(f"ModelerAgent 调用工具: {tool_name}")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(content=f"建模手调用{tool_name}工具"),
|
||||
)
|
||||
|
||||
await self.append_chat_history(msg.model_dump())
|
||||
|
||||
try:
|
||||
result = await tool_registry.dispatch(
|
||||
tool_name, tool_args, self.task_id
|
||||
)
|
||||
except ValueError as e:
|
||||
result = f"工具调用失败: {e}"
|
||||
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": tool_id,
|
||||
"name": tool_name,
|
||||
"content": result,
|
||||
}
|
||||
)
|
||||
|
||||
# 工具结果返回后,再次请求 LLM 生成最终 JSON
|
||||
next_response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
agent_name=self.__class__.__name__,
|
||||
)
|
||||
json_str = next_response.choices[0].message.content
|
||||
else:
|
||||
json_str = msg.content
|
||||
|
||||
if not json_str:
|
||||
raise ValueError("返回的 JSON 字符串为空,请检查输入内容。")
|
||||
|
||||
questions_solution = repair_json(json_str)
|
||||
if questions_solution:
|
||||
ic(questions_solution)
|
||||
return ModelerToCoder(questions_solution=questions_solution)
|
||||
|
||||
logger.warning(
|
||||
f"JSON 解析失败 (第{attempt + 1}/{self.max_retries + 1}次),请求模型重新生成"
|
||||
)
|
||||
await self.append_chat_history({"role": "assistant", "content": json_str})
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "user",
|
||||
"content": '你返回的JSON格式有误,请严格按照JSON格式重新输出,注意字符串值内的双引号必须转义为\\",不要包含未转义的特殊字符。',
|
||||
}
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
f"ModelerAgent 超过最大重试次数({self.max_retries}),无法解析 JSON 响应"
|
||||
)
|
||||
|
||||
@@ -4,18 +4,15 @@ from app.core.agents.agent import Agent
|
||||
from app.core.llm.llm import LLM
|
||||
from app.core.prompts import get_writer_prompt
|
||||
from app.schemas.enums import CompTemplate, FormatOutPut
|
||||
from app.config.setting import ApiType
|
||||
from app.tools.openalex_scholar import OpenAlexScholar
|
||||
from app.utils.log_util import logger
|
||||
from app.services.redis_manager import redis_manager
|
||||
from app.schemas.response import SystemMessage, WriterMessage
|
||||
import json
|
||||
from app.core.functions import writer_tools
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
from app.core.functions import writer_tools, writer_tools_anthropic
|
||||
from app.schemas.A2A import WriterResponse
|
||||
|
||||
# 写作手默认最大重试次数
|
||||
DEFAULT_WRITER_MAX_RETRIES = 3
|
||||
|
||||
|
||||
# TODO: 并行 parallel
|
||||
# TODO: 获取当前文件下的文件
|
||||
@@ -28,21 +25,18 @@ class WriterAgent(Agent):
|
||||
self,
|
||||
task_id: str,
|
||||
model: LLM,
|
||||
max_chat_turns: int | None = None, # 最大对话轮次,None表示无限制
|
||||
comp_template: CompTemplate = CompTemplate.CHINA,
|
||||
format_output: FormatOutPut = FormatOutPut.Markdown,
|
||||
scholar: OpenAlexScholar | None = None,
|
||||
max_memory: int = 25, # 添加最大记忆轮次
|
||||
max_retries: int = DEFAULT_WRITER_MAX_RETRIES,
|
||||
context_window: int = 128000,
|
||||
) -> None:
|
||||
super().__init__(task_id, model, max_chat_turns, max_memory)
|
||||
super().__init__(task_id, model, context_window)
|
||||
self.format_out_put = format_output
|
||||
self.comp_template = comp_template
|
||||
self.scholar = scholar
|
||||
self.is_first_run = True
|
||||
self.system_prompt = get_writer_prompt(format_output)
|
||||
self.available_images: list[str] = []
|
||||
self.max_retries = max_retries
|
||||
|
||||
async def run( # type: ignore[reportIncompatibleMethodOverride]
|
||||
self,
|
||||
@@ -50,18 +44,19 @@ class WriterAgent(Agent):
|
||||
available_images: list[str] | None = None,
|
||||
sub_title: str | None = None,
|
||||
) -> WriterResponse:
|
||||
"""执行写作任务,支持有限重试和软降级。
|
||||
|
||||
"""
|
||||
执行写作任务
|
||||
Args:
|
||||
prompt: 写作提示。
|
||||
available_images: 可用的图片相对路径列表。
|
||||
sub_title: 子任务标题。
|
||||
|
||||
Returns:
|
||||
WriterResponse 对象,最终失败时包含错误信息。
|
||||
prompt: 写作提示
|
||||
available_images: 可用的图片相对路径列表(如 20250420-173744-9f87792c/编号_分布.png)
|
||||
sub_title: 子任务标题
|
||||
"""
|
||||
logger.info(f"subtitle是:{sub_title}")
|
||||
|
||||
# 根据 api_type 选择 tools 格式
|
||||
api_type = self.model.api_type
|
||||
tools = writer_tools_anthropic if api_type == ApiType.ANTHROPIC else writer_tools
|
||||
|
||||
if self.is_first_run:
|
||||
self.is_first_run = False
|
||||
await self.append_chat_history(
|
||||
@@ -83,102 +78,88 @@ class WriterAgent(Agent):
|
||||
prompt = prompt + image_prompt
|
||||
|
||||
logger.info(f"{self.__class__.__name__}:开始:执行对话")
|
||||
self.current_chat_turns += 1
|
||||
|
||||
await self.append_chat_history({"role": "user", "content": prompt})
|
||||
|
||||
last_error: str = ""
|
||||
for attempt in range(self.max_retries + 1):
|
||||
try:
|
||||
# 获取历史消息用于本次对话
|
||||
response = await self.model.chat(
|
||||
# 获取历史消息用于本次对话
|
||||
response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
tools=tools,
|
||||
tool_choice="auto",
|
||||
agent_name=self.__class__.__name__,
|
||||
sub_title=sub_title,
|
||||
)
|
||||
|
||||
footnotes = []
|
||||
response_content: str = ""
|
||||
|
||||
if response.tool_calls:
|
||||
logger.info("检测到工具调用")
|
||||
tool_call = response.tool_calls[0]
|
||||
tool_id = tool_call.id
|
||||
if tool_call.name == "search_papers":
|
||||
logger.info("调用工具: search_papers")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(content=f"写作手调用{tool_call.name}工具"),
|
||||
)
|
||||
|
||||
query = json.loads(tool_call.arguments)["query"]
|
||||
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
WriterMessage(
|
||||
content=query,
|
||||
),
|
||||
)
|
||||
|
||||
# 更新对话历史 - 添加助手的响应
|
||||
assistant_msg: dict = {"role": "assistant", "content": response.content}
|
||||
if response.tool_calls:
|
||||
assistant_msg["tool_calls"] = [
|
||||
{
|
||||
"id": tc.id,
|
||||
"type": "function",
|
||||
"function": {"name": tc.name, "arguments": tc.arguments},
|
||||
}
|
||||
for tc in response.tool_calls
|
||||
]
|
||||
await self.append_chat_history(assistant_msg)
|
||||
|
||||
try:
|
||||
assert self.scholar is not None, "scholar 未初始化"
|
||||
papers = await self.scholar.search_papers(query)
|
||||
except Exception as e:
|
||||
error_msg = f"搜索文献失败: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
return WriterResponse(
|
||||
response_content=error_msg, footnotes=footnotes
|
||||
)
|
||||
# TODO: pass to frontend
|
||||
assert self.scholar is not None, "scholar 未初始化"
|
||||
papers_str = self.scholar.papers_to_str(papers)
|
||||
logger.info(f"搜索文献结果\n{papers_str}")
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "tool",
|
||||
"content": papers_str,
|
||||
"tool_call_id": tool_id,
|
||||
"name": "search_papers",
|
||||
}
|
||||
)
|
||||
next_response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
tools=writer_tools,
|
||||
tools=tools,
|
||||
tool_choice="auto",
|
||||
agent_name=self.__class__.__name__,
|
||||
sub_title=sub_title,
|
||||
)
|
||||
|
||||
footnotes = []
|
||||
response_content: str = ""
|
||||
|
||||
if (
|
||||
hasattr(response.choices[0].message, "tool_calls")
|
||||
and response.choices[0].message.tool_calls
|
||||
):
|
||||
logger.info("检测到工具调用")
|
||||
tool_call = response.choices[0].message.tool_calls[0]
|
||||
tool_id = tool_call.id
|
||||
if tool_call.function.name == "search_papers":
|
||||
logger.info("调用工具: search_papers")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(content=f"写作手调用{tool_call.function.name}工具"),
|
||||
)
|
||||
|
||||
query = json.loads(tool_call.function.arguments)["query"]
|
||||
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
WriterMessage(
|
||||
content=query,
|
||||
),
|
||||
)
|
||||
|
||||
# 更新对话历史 - 添加助手的响应
|
||||
await self.append_chat_history(response.choices[0].message.model_dump())
|
||||
ic(response.choices[0].message.model_dump())
|
||||
|
||||
assert self.scholar is not None, "scholar 未初始化"
|
||||
papers = await self.scholar.search_papers(query)
|
||||
|
||||
# TODO: pass to frontend
|
||||
papers_str = self.scholar.papers_to_str(papers)
|
||||
logger.info(f"搜索文献结果\n{papers_str}")
|
||||
await self.append_chat_history(
|
||||
{
|
||||
"role": "tool",
|
||||
"content": papers_str,
|
||||
"tool_call_id": tool_id,
|
||||
"name": "search_papers",
|
||||
}
|
||||
)
|
||||
next_response = await self.model.chat(
|
||||
history=self.chat_history,
|
||||
tools=writer_tools,
|
||||
tool_choice="auto",
|
||||
agent_name=self.__class__.__name__,
|
||||
sub_title=sub_title,
|
||||
)
|
||||
response_content = next_response.choices[0].message.content
|
||||
else:
|
||||
response_content = response.choices[0].message.content
|
||||
self.chat_history.append({"role": "assistant", "content": response_content})
|
||||
logger.info(f"{self.__class__.__name__}:完成:执行对话")
|
||||
return WriterResponse(response_content=response_content, footnotes=footnotes)
|
||||
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
logger.warning(
|
||||
f"WriterAgent 执行失败 (尝试 {attempt + 1}/{self.max_retries + 1}): {last_error}"
|
||||
)
|
||||
if attempt < self.max_retries:
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content=f"写作手执行出错,正在重试({attempt + 1}/{self.max_retries})...",
|
||||
type="warning",
|
||||
),
|
||||
)
|
||||
|
||||
# 所有重试均失败,软降级返回错误信息
|
||||
error_msg = f"写作手超过最大重试次数({self.max_retries}),最后错误: {last_error}"
|
||||
logger.error(error_msg)
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(content=error_msg, type="error"),
|
||||
)
|
||||
return WriterResponse(response_content=error_msg, footnotes=[])
|
||||
response_content = next_response.content or ""
|
||||
else:
|
||||
response_content = response.content or ""
|
||||
self.chat_history.append({"role": "assistant", "content": response_content})
|
||||
logger.info(f"{self.__class__.__name__}:完成:执行对话")
|
||||
return WriterResponse(response_content=response_content, footnotes=footnotes)
|
||||
|
||||
async def summarize(self) -> str:
|
||||
"""总结对话内容,生成任务执行摘要。"""
|
||||
@@ -190,10 +171,11 @@ class WriterAgent(Agent):
|
||||
response = await self.model.chat(
|
||||
history=self.chat_history, agent_name=self.__class__.__name__
|
||||
)
|
||||
response_content = response.content or ""
|
||||
await self.append_chat_history(
|
||||
{"role": "assistant", "content": response.choices[0].message.content}
|
||||
{"role": "assistant", "content": response_content}
|
||||
)
|
||||
return response.choices[0].message.content
|
||||
return response_content
|
||||
except Exception as e:
|
||||
logger.error(f"总结生成失败: {str(e)}")
|
||||
# 返回一个基础总结,避免完全失败
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
"""评估器模块,用于评估 Agent 输出质量(Shadow Mode)。"""
|
||||
|
||||
import json
|
||||
from app.core.llm.llm import LLM, simple_chat
|
||||
from app.schemas.A2A import EvaluationResult
|
||||
from app.utils.log_util import logger
|
||||
|
||||
|
||||
# 评估器 system prompt
|
||||
EVALUATOR_SYSTEM_PROMPT = """你是一个数学建模任务评估器。你需要评估 Agent 的输出质量。
|
||||
|
||||
请以 JSON 格式返回评估结果,包含以下字段:
|
||||
- passed: bool,是否通过评估
|
||||
- score: float,0-1 之间的评分
|
||||
- feedback: str,具体的改进建议(中文)
|
||||
- should_handoff: bool,是否建议切换到备用模型
|
||||
- reason: str,评估理由(中文)
|
||||
|
||||
评估标准:
|
||||
1. 输出是否完整、格式是否正确
|
||||
2. 内容是否合理、有逻辑
|
||||
3. 是否满足任务要求
|
||||
|
||||
严格输出 JSON 格式,不要包含其他内容。"""
|
||||
|
||||
|
||||
class Evaluator:
|
||||
"""评估器,使用独立的便宜 LLM 对 Agent 输出做单次评估。
|
||||
|
||||
Shadow Mode 下只记录评估结果,不触发重跑。
|
||||
评估失败时默认 passed=True,不阻塞主流程。
|
||||
"""
|
||||
|
||||
def __init__(self, model: LLM) -> None:
|
||||
"""初始化评估器。
|
||||
|
||||
Args:
|
||||
model: 评估用 LLM 实例(独立的便宜模型)。
|
||||
"""
|
||||
self.model = model
|
||||
|
||||
async def evaluate(self, task_description: str, agent_output: str) -> EvaluationResult:
|
||||
"""评估 Agent 输出质量。
|
||||
|
||||
Args:
|
||||
task_description: 任务描述(原始 prompt)。
|
||||
agent_output: Agent 的输出内容。
|
||||
|
||||
Returns:
|
||||
EvaluationResult 评估结果,评估失败时返回 passed=True 的默认结果。
|
||||
"""
|
||||
if not self.model.model or not self.model.api_key:
|
||||
logger.debug("评估器未配置,跳过评估")
|
||||
return EvaluationResult()
|
||||
|
||||
user_prompt = (
|
||||
f"【任务描述】\n{task_description}\n\n"
|
||||
f"【Agent 输出】\n{agent_output}"
|
||||
)
|
||||
|
||||
history = [
|
||||
{"role": "system", "content": EVALUATOR_SYSTEM_PROMPT},
|
||||
{"role": "user", "content": user_prompt},
|
||||
]
|
||||
|
||||
try:
|
||||
content = await simple_chat(self.model, history)
|
||||
# 清理 JSON 字符串
|
||||
content = content.replace("```json", "").replace("```", "").strip()
|
||||
data = json.loads(content)
|
||||
return EvaluationResult(
|
||||
passed=data.get("passed", True),
|
||||
score=data.get("score", 1.0),
|
||||
feedback=data.get("feedback", ""),
|
||||
should_handoff=data.get("should_handoff", False),
|
||||
reason=data.get("reason", ""),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"评估器执行失败,默认通过: {e}")
|
||||
return EvaluationResult()
|
||||
@@ -3,12 +3,10 @@
|
||||
from app.models.user_output import UserOutput
|
||||
from app.tools.base_interpreter import BaseCodeInterpreter
|
||||
from app.core.agents.modeler_agent import ModelerToCoder
|
||||
from app.schemas.evidence import DataEvidence, KnowledgeEvidence
|
||||
|
||||
|
||||
class Flows:
|
||||
"""管理数学建模任务的求解流程和写作流程。"""
|
||||
|
||||
def __init__(self, questions: dict[str, str | int]):
|
||||
self.flows: dict[str, dict] = {}
|
||||
self.questions: dict[str, str | int] = questions
|
||||
@@ -34,19 +32,13 @@ class Flows:
|
||||
self.flows = {key: {} for key in seq}
|
||||
|
||||
def get_solution_flows(
|
||||
self,
|
||||
questions: dict[str, str | int],
|
||||
modeler_response: ModelerToCoder,
|
||||
search_evidence: list[DataEvidence] | None = None,
|
||||
coder_knowledge: list[KnowledgeEvidence] | None = None,
|
||||
self, questions: dict[str, str | int], modeler_response: ModelerToCoder
|
||||
):
|
||||
"""生成求解阶段的流程配置。
|
||||
|
||||
Args:
|
||||
questions: 包含各问题描述的字典。
|
||||
modeler_response: 建模手的响应,包含各问题的解决方案。
|
||||
search_evidence: Web 搜索到的数据证据列表,注入到 coder prompt 中。
|
||||
coder_knowledge: RAG 检索的代码模板知识,注入到 coder prompt 中。
|
||||
|
||||
Returns:
|
||||
求解流程配置字典,键为任务名,值包含 coder_prompt 等信息。
|
||||
@@ -57,43 +49,11 @@ class Flows:
|
||||
if key.startswith("ques") and key != "ques_count"
|
||||
}
|
||||
solutions = modeler_response.questions_solution
|
||||
|
||||
# 构造搜索结果注入文本
|
||||
evidence_text = ""
|
||||
if search_evidence:
|
||||
evidence_lines = []
|
||||
for ev in search_evidence:
|
||||
line = f"- {ev.content}"
|
||||
if ev.unit:
|
||||
line += f" (单位: {ev.unit})"
|
||||
if ev.time_range:
|
||||
line += f" (时间: {ev.time_range})"
|
||||
if ev.region:
|
||||
line += f" (地域: {ev.region})"
|
||||
if ev.source_url:
|
||||
line += f" (来源: {ev.source_url})"
|
||||
evidence_lines.append(line)
|
||||
evidence_text = (
|
||||
"\n【搜索到的真实数据】\n" + "\n".join(evidence_lines) + "\n"
|
||||
)
|
||||
|
||||
# 构造知识库参考注入文本
|
||||
knowledge_text = ""
|
||||
if coder_knowledge:
|
||||
knowledge_lines = []
|
||||
for ke in coder_knowledge:
|
||||
line = f"- {ke.content[:300]}"
|
||||
if ke.source_title:
|
||||
line += f" (来源: {ke.source_title})"
|
||||
knowledge_lines.append(line)
|
||||
knowledge_text = "\n【代码模板参考】\n" + "\n".join(knowledge_lines) + "\n"
|
||||
|
||||
ques_flow = {
|
||||
key: {
|
||||
"coder_prompt": f"""
|
||||
参考建模手给出的解决方案{solutions.get(key, "")}
|
||||
完成如下问题{value}
|
||||
{evidence_text}{knowledge_text}
|
||||
""",
|
||||
}
|
||||
for key, value in questions_quesx.items()
|
||||
@@ -103,7 +63,6 @@ class Flows:
|
||||
"coder_prompt": f"""
|
||||
参考建模手给出的解决方案{solutions.get("eda", "对数据进行探索性分析")}
|
||||
对当前目录下数据进行EDA分析(数据清洗,可视化),清洗后的数据保存当前目录下,**不需要复杂的模型**
|
||||
{evidence_text}{knowledge_text}
|
||||
""",
|
||||
},
|
||||
**ques_flow,
|
||||
@@ -111,18 +70,13 @@ class Flows:
|
||||
"coder_prompt": f"""
|
||||
参考建模手给出的解决方案{solutions.get("sensitivity_analysis", "对模型进行灵敏度分析")}
|
||||
完成敏感性分析
|
||||
{evidence_text}{knowledge_text}
|
||||
""",
|
||||
},
|
||||
}
|
||||
return flows
|
||||
|
||||
def get_write_flows(
|
||||
self,
|
||||
user_output: UserOutput,
|
||||
config_template: dict,
|
||||
bg_ques_all: str,
|
||||
writer_knowledge: list[KnowledgeEvidence] | None = None,
|
||||
self, user_output: UserOutput, config_template: dict, bg_ques_all: str
|
||||
):
|
||||
"""生成写作阶段的流程配置。
|
||||
|
||||
@@ -130,31 +84,18 @@ class Flows:
|
||||
user_output: 用户输出对象,包含已求解的结果。
|
||||
config_template: 论文模板配置。
|
||||
bg_ques_all: 问题背景和题目信息。
|
||||
writer_knowledge: RAG 检索的论文写作模板知识。
|
||||
|
||||
Returns:
|
||||
写作流程配置字典,键为章节名,值为写作提示。
|
||||
"""
|
||||
model_build_solve = user_output.get_model_build_solve()
|
||||
|
||||
# 构造写作知识参考注入文本
|
||||
knowledge_text = ""
|
||||
if writer_knowledge:
|
||||
knowledge_lines = []
|
||||
for ke in writer_knowledge:
|
||||
line = f"- {ke.content[:300]}"
|
||||
if ke.source_title:
|
||||
line += f" (来源: {ke.source_title})"
|
||||
knowledge_lines.append(line)
|
||||
knowledge_text = "\n【写作模板参考】\n" + "\n".join(knowledge_lines) + "\n"
|
||||
|
||||
flows = {
|
||||
"firstPage": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["firstPage"]},撰写标题,摘要,关键词{knowledge_text}""",
|
||||
"RepeatQues": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["RepeatQues"]},撰写问题重述{knowledge_text}""",
|
||||
"analysisQues": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["analysisQues"]},撰写问题分析{knowledge_text}""",
|
||||
"modelAssumption": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["modelAssumption"]},撰写模型假设{knowledge_text}""",
|
||||
"symbol": f"""不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["symbol"]},撰写符号说明部分{knowledge_text}""",
|
||||
"judge": f"""不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["judge"]},撰写模型的评价部分{knowledge_text}""",
|
||||
"firstPage": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["firstPage"]},撰写标题,摘要,关键词""",
|
||||
"RepeatQues": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["RepeatQues"]},撰写问题重述""",
|
||||
"analysisQues": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["analysisQues"]},撰写问题分析""",
|
||||
"modelAssumption": f"""问题背景{bg_ques_all},不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["modelAssumption"]},撰写模型假设""",
|
||||
"symbol": f"""不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["symbol"]},撰写符号说明部分""",
|
||||
"judge": f"""不需要编写代码,根据模型的求解的信息{model_build_solve},按照如下模板撰写:{config_template["judge"]},撰写模型的评价部分""",
|
||||
}
|
||||
return flows
|
||||
|
||||
@@ -164,39 +105,23 @@ class Flows:
|
||||
coder_response: str,
|
||||
code_interpreter: BaseCodeInterpreter,
|
||||
config_template: dict,
|
||||
writer_knowledge: list[KnowledgeEvidence] | None = None,
|
||||
) -> str:
|
||||
"""根据不同的key生成对应的writer_prompt
|
||||
|
||||
Args:
|
||||
key: 任务类型
|
||||
coder_response: 代码执行结果
|
||||
code_interpreter: 代码解释器
|
||||
config_template: 论文模板配置
|
||||
writer_knowledge: RAG 检索的论文写作模板知识
|
||||
|
||||
Returns:
|
||||
str: 生成的writer_prompt
|
||||
"""
|
||||
code_output = code_interpreter.get_code_output(key)
|
||||
|
||||
# 构造写作知识参考注入文本
|
||||
knowledge_text = ""
|
||||
if writer_knowledge:
|
||||
knowledge_lines = []
|
||||
for ke in writer_knowledge:
|
||||
line = f"- {ke.content[:300]}"
|
||||
if ke.source_title:
|
||||
line += f" (来源: {ke.source_title})"
|
||||
knowledge_lines.append(line)
|
||||
knowledge_text = "\n【写作模板参考】\n" + "\n".join(knowledge_lines) + "\n"
|
||||
|
||||
questions_quesx_keys = self.get_questions_quesx_keys()
|
||||
bgc = self.questions["background"]
|
||||
quesx_writer_prompt = {
|
||||
key: f"""
|
||||
问题背景{bgc},不需要编写代码,代码手得到的结果{coder_response},{code_output},按照如下模板撰写:{config_template[key]}
|
||||
{knowledge_text}
|
||||
"""
|
||||
for key in questions_quesx_keys
|
||||
}
|
||||
@@ -204,12 +129,10 @@ class Flows:
|
||||
writer_prompt = {
|
||||
"eda": f"""
|
||||
问题背景{bgc},不需要编写代码,代码手得到的结果{coder_response},{code_output},按照如下模板撰写:{config_template["eda"]}
|
||||
{knowledge_text}
|
||||
""",
|
||||
**quesx_writer_prompt,
|
||||
"sensitivity_analysis": f"""
|
||||
问题背景{bgc},不需要编写代码,代码手得到的结果{coder_response},{code_output},按照如下模板撰写:{config_template["sensitivity_analysis"]}
|
||||
{knowledge_text}
|
||||
""",
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""工具函数定义模块,为各 Agent 提供可用的工具 schema。"""
|
||||
|
||||
# ---- OpenAI 格式(Chat Completions + Responses 共用) ----
|
||||
|
||||
coder_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
@@ -23,74 +25,6 @@ coder_tools = [
|
||||
},
|
||||
]
|
||||
|
||||
# have installed: numpy scipy pandas matplotlib seaborn scikit-learn xgboost
|
||||
|
||||
# TODO: pip install python
|
||||
|
||||
# TODO: read files
|
||||
|
||||
# TODO: get_cites
|
||||
|
||||
|
||||
# Web 搜索工具 schema
|
||||
search_web_tool = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_web",
|
||||
"description": "Search the web for real-world data, statistics, and facts. Returns structured data evidence with source URLs, units, time ranges, and regions.",
|
||||
"strict": True,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query, be specific about what data you need",
|
||||
},
|
||||
"data_type": {
|
||||
"type": "string",
|
||||
"description": "Type of data expected: 'statistical', 'timeseries', 'categorical', or 'general'",
|
||||
},
|
||||
"max_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results to return (1-10)",
|
||||
},
|
||||
},
|
||||
"required": ["query", "data_type", "max_results"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# RAG 知识检索工具 schema
|
||||
search_knowledge_tool = {
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "search_knowledge",
|
||||
"description": "Search the knowledge base for mathematical modeling methods, code templates, and paper writing references.",
|
||||
"strict": True,
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query describing what knowledge you need",
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "Knowledge scope: 'method' for modeling methods, 'code' for code templates, 'paper' for writing references",
|
||||
},
|
||||
"method_name": {
|
||||
"type": "string",
|
||||
"description": "Specific method name to search for (e.g. 'TOPSIS', 'AHP'), or empty string for general search",
|
||||
},
|
||||
},
|
||||
"required": ["query", "scope", "method_name"],
|
||||
"additionalProperties": False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
## writeragent tools
|
||||
writer_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
@@ -109,3 +43,37 @@ writer_tools = [
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
# ---- Anthropic 格式 ----
|
||||
|
||||
coder_tools_anthropic = [
|
||||
{
|
||||
"name": "execute_code",
|
||||
"description": "This function allows you to execute Python code and retrieve the terminal output. If the code "
|
||||
"generates image output, the function will return the text '[image]'. The code is sent to a "
|
||||
"Jupyter kernel for execution. The kernel will remain active after execution, retaining all "
|
||||
"variables in memory."
|
||||
"You cannot show rich outputs like plots or images, but you can store them in the working directory and point the user to them. ",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"code": {"type": "string", "description": "The code text"}
|
||||
},
|
||||
"required": ["code"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
writer_tools_anthropic = [
|
||||
{
|
||||
"name": "search_papers",
|
||||
"description": "Search for papers using a query string.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "The query string"}
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
+60
-142
@@ -12,40 +12,49 @@ from app.schemas.response import (
|
||||
CoordinatorMessage,
|
||||
)
|
||||
from app.services.redis_manager import redis_manager
|
||||
from litellm import acompletion # type: ignore[import-unresolved]
|
||||
import litellm # type: ignore[import-unresolved]
|
||||
from app.schemas.enums import AgentType
|
||||
from app.utils.track import agent_metrics
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
from app.config.setting import ApiType
|
||||
from app.core.llm.types import StandardResponse
|
||||
from app.core.llm.providers.base import BaseProvider
|
||||
from app.core.llm.providers.openai_chat import OpenAIChatProvider
|
||||
from app.core.llm.providers.openai_responses import OpenAIResponsesProvider
|
||||
from app.core.llm.providers.anthropic import AnthropicProvider
|
||||
|
||||
litellm.callbacks = [agent_metrics]
|
||||
|
||||
class LLM:
|
||||
"""大语言模型封装类,提供对话调用、重试和工具调用验证功能。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
api_type: ApiType | None = None,
|
||||
api_key: str | None = None,
|
||||
model: str | None = None,
|
||||
base_url: str | None = None,
|
||||
task_id: str = "",
|
||||
max_tokens: int | None = None,
|
||||
):
|
||||
self.api_type = api_type
|
||||
self.api_key = api_key
|
||||
self.model = model
|
||||
self.base_url = base_url
|
||||
self.chat_count = 0
|
||||
self.max_tokens = max_tokens
|
||||
self.task_id = task_id
|
||||
self.provider = self._create_provider(api_type)
|
||||
|
||||
def _create_provider(self, api_type: ApiType | None) -> BaseProvider:
|
||||
"""根据 api_type 创建对应的 Provider。"""
|
||||
match api_type:
|
||||
case ApiType.OPENAI_RESPONSES:
|
||||
return OpenAIResponsesProvider()
|
||||
case ApiType.ANTHROPIC:
|
||||
return AnthropicProvider()
|
||||
case _:
|
||||
# 默认使用 OpenAI Chat Completions(兼容未配置 api_type 的情况)
|
||||
return OpenAIChatProvider()
|
||||
|
||||
def _validate_config(self, agent_name: str) -> None:
|
||||
"""验证 LLM 配置是否完整。
|
||||
|
||||
Args:
|
||||
agent_name: Agent 类型名称,用于错误提示。
|
||||
|
||||
Raises:
|
||||
ValueError: 模型 ID 或 API Key 未配置时抛出。
|
||||
"""
|
||||
"""验证 LLM 配置是否完整。"""
|
||||
if not self.model or not str(self.model).strip():
|
||||
raise ValueError(f"{agent_name} 未配置模型 ID,请设置对应的 *_MODEL")
|
||||
if not self.api_key or not str(self.api_key).strip():
|
||||
@@ -56,47 +65,34 @@ class LLM:
|
||||
history: list | None = None,
|
||||
tools: list | None = None,
|
||||
tool_choice: str | None = None,
|
||||
max_retries: int | None = None, # 最大重试次数,None表示无限制
|
||||
retry_delay: float = 1.0, # 添加重试延迟
|
||||
top_p: float | None = None, # 添加top_p参数,
|
||||
agent_name: str = "SystemAgent", # CoderAgent or WriterAgent
|
||||
max_retries: int | None = None,
|
||||
retry_delay: float = 1.0,
|
||||
top_p: float | None = None,
|
||||
agent_name: str = "SystemAgent",
|
||||
sub_title: str | None = None,
|
||||
) -> Any:
|
||||
logger.info(f"subtitle是:{sub_title}")
|
||||
) -> StandardResponse:
|
||||
self._validate_config(agent_name)
|
||||
|
||||
# 验证和修复工具调用完整性
|
||||
# 验证和修复工具调用完整性(仅对 OpenAI 格式的历史有效)
|
||||
if history:
|
||||
history = self._validate_and_fix_tool_calls(history)
|
||||
|
||||
kwargs = {
|
||||
"api_key": self.api_key,
|
||||
"model": self.model,
|
||||
"messages": history,
|
||||
"stream": False,
|
||||
"top_p": top_p,
|
||||
"metadata": {"agent_name": agent_name},
|
||||
}
|
||||
messages = history or []
|
||||
|
||||
if tools:
|
||||
kwargs["tools"] = tools
|
||||
kwargs["tool_choice"] = tool_choice
|
||||
|
||||
if self.max_tokens:
|
||||
kwargs["max_tokens"] = self.max_tokens
|
||||
|
||||
if self.base_url:
|
||||
kwargs["base_url"] = self.base_url
|
||||
litellm.enable_json_schema_validation = True #加入json格式验证
|
||||
|
||||
# TODO: stream 输出
|
||||
attempt = 0
|
||||
while True:
|
||||
try:
|
||||
response = await acompletion(**kwargs)
|
||||
logger.info(f"API返回: {response}")
|
||||
if not response or not hasattr(response, "choices"):
|
||||
raise ValueError("无效的API响应")
|
||||
response = await self.provider.call(
|
||||
messages=messages,
|
||||
model=self.model, # type: ignore[arg-type]
|
||||
api_key=self.api_key, # type: ignore[arg-type]
|
||||
base_url=self.base_url,
|
||||
tools=tools,
|
||||
tool_choice=tool_choice,
|
||||
max_tokens=self.max_tokens,
|
||||
top_p=top_p,
|
||||
)
|
||||
logger.info(f"API返回: content={response.content!r}, tool_calls={len(response.tool_calls)}")
|
||||
self.chat_count += 1
|
||||
await self.send_message(response, agent_name, sub_title)
|
||||
return response
|
||||
@@ -104,78 +100,47 @@ class LLM:
|
||||
attempt += 1
|
||||
logger.error(f"第{attempt}次重试: {str(e)}")
|
||||
if max_retries is not None and attempt >= max_retries:
|
||||
logger.debug(f"请求参数: {kwargs}")
|
||||
raise
|
||||
time.sleep(retry_delay * min(attempt, 10)) # 指数退避,上限10秒
|
||||
time.sleep(retry_delay * min(attempt, 10))
|
||||
|
||||
def _validate_and_fix_tool_calls(self, history: list) -> list:
|
||||
"""验证并修复工具调用完整性"""
|
||||
"""验证并修复工具调用完整性。"""
|
||||
if not history:
|
||||
return history
|
||||
|
||||
ic(f"🔍 开始验证工具调用,历史消息数量: {len(history)}")
|
||||
|
||||
# 查找所有未匹配的tool_calls
|
||||
fixed_history = []
|
||||
i = 0
|
||||
|
||||
while i < len(history):
|
||||
msg = history[i]
|
||||
|
||||
# 如果是包含tool_calls的消息
|
||||
if isinstance(msg, dict) and "tool_calls" in msg and msg["tool_calls"]:
|
||||
ic(f"📞 发现tool_calls消息在位置 {i}")
|
||||
|
||||
# 检查每个tool_call是否都有对应的response,分别处理
|
||||
valid_tool_calls = []
|
||||
invalid_tool_calls = []
|
||||
|
||||
for tool_call in msg["tool_calls"]:
|
||||
tool_call_id = tool_call.get("id")
|
||||
ic(f" 检查tool_call_id: {tool_call_id}")
|
||||
|
||||
if tool_call_id:
|
||||
# 查找对应的tool响应
|
||||
found_response = False
|
||||
for j in range(i + 1, len(history)):
|
||||
if (
|
||||
history[j].get("role") == "tool"
|
||||
and history[j].get("tool_call_id") == tool_call_id
|
||||
):
|
||||
ic(f" ✅ 找到匹配响应在位置 {j}")
|
||||
found_response = True
|
||||
break
|
||||
|
||||
if found_response:
|
||||
valid_tool_calls.append(tool_call)
|
||||
else:
|
||||
ic(f" ❌ 未找到匹配响应: {tool_call_id}")
|
||||
invalid_tool_calls.append(tool_call)
|
||||
|
||||
# 根据检查结果处理消息
|
||||
if valid_tool_calls:
|
||||
# 有有效的tool_calls,保留它们
|
||||
fixed_msg = msg.copy()
|
||||
fixed_msg["tool_calls"] = valid_tool_calls
|
||||
fixed_history.append(fixed_msg)
|
||||
ic(
|
||||
f" 🔧 保留 {len(valid_tool_calls)} 个有效tool_calls,移除 {len(invalid_tool_calls)} 个无效的"
|
||||
)
|
||||
else:
|
||||
# 没有有效的tool_calls,移除tool_calls但可能保留其他内容
|
||||
cleaned_msg = {k: v for k, v in msg.items() if k != "tool_calls"}
|
||||
if cleaned_msg.get("content"):
|
||||
fixed_history.append(cleaned_msg)
|
||||
ic(" 🔧 移除所有tool_calls,保留消息内容")
|
||||
else:
|
||||
ic(" 🗑️ 完全移除空的tool_calls消息")
|
||||
|
||||
# 如果是tool响应消息,检查是否是孤立的
|
||||
elif isinstance(msg, dict) and msg.get("role") == "tool":
|
||||
tool_call_id = msg.get("tool_call_id")
|
||||
ic(f"🔧 检查tool响应消息: {tool_call_id}")
|
||||
|
||||
# 查找对应的tool_calls
|
||||
found_call = False
|
||||
for j in range(len(fixed_history)):
|
||||
if fixed_history[j].get("tool_calls") and any(
|
||||
@@ -184,38 +149,24 @@ class LLM:
|
||||
):
|
||||
found_call = True
|
||||
break
|
||||
|
||||
if found_call:
|
||||
fixed_history.append(msg)
|
||||
ic(" ✅ 保留有效的tool响应")
|
||||
else:
|
||||
ic(f" 🗑️ 移除孤立的tool响应: {tool_call_id}")
|
||||
|
||||
else:
|
||||
# 普通消息,直接保留
|
||||
fixed_history.append(msg)
|
||||
|
||||
i += 1
|
||||
|
||||
if len(fixed_history) != len(history):
|
||||
ic(f"🔧 修复完成: {len(history)} -> {len(fixed_history)} 条消息")
|
||||
else:
|
||||
ic("✅ 验证通过,无需修复")
|
||||
|
||||
return fixed_history
|
||||
|
||||
async def send_message(self, response, agent_name, sub_title=None):
|
||||
"""将 LLM 响应通过 Redis 发送给前端。
|
||||
async def send_message(
|
||||
self,
|
||||
response: StandardResponse,
|
||||
agent_name: str,
|
||||
sub_title: str | None = None,
|
||||
):
|
||||
"""将 LLM 响应通过 Redis 发送给前端。"""
|
||||
content = response.content
|
||||
|
||||
Args:
|
||||
response: LLM 返回的响应对象。
|
||||
agent_name: Agent 类型。
|
||||
sub_title: 子任务标题。
|
||||
"""
|
||||
logger.info(f"subtitle是:{sub_title}")
|
||||
content = response.choices[0].message.content
|
||||
|
||||
# tool_call 响应的 content 为 None,跳过消息发送
|
||||
if content is None:
|
||||
return
|
||||
|
||||
@@ -224,13 +175,9 @@ class LLM:
|
||||
case AgentType.CODER:
|
||||
agent_msg = CoderMessage(content=content)
|
||||
case AgentType.WRITER:
|
||||
# 处理 Markdown 格式的图片语法
|
||||
content, _ = split_footnotes(content)
|
||||
content = transform_link(self.task_id, content)
|
||||
agent_msg = WriterMessage(
|
||||
content=content,
|
||||
sub_title=sub_title,
|
||||
)
|
||||
agent_msg = WriterMessage(content=content, sub_title=sub_title)
|
||||
case AgentType.MODELER:
|
||||
agent_msg = ModelerMessage(content=content)
|
||||
case AgentType.SYSTEM:
|
||||
@@ -240,44 +187,15 @@ class LLM:
|
||||
case _:
|
||||
raise ValueError(f"不支持的agent类型: {agent_name}")
|
||||
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
agent_msg,
|
||||
)
|
||||
|
||||
|
||||
# class DeepSeekModel(LLM):
|
||||
# def __init__(
|
||||
# self,
|
||||
# api_key: str,
|
||||
# model: str,
|
||||
# base_url: str,
|
||||
# task_id: str,
|
||||
# ):
|
||||
# super().__init__(api_key, model, base_url, task_id)
|
||||
# self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
|
||||
await redis_manager.publish_message(self.task_id, agent_msg)
|
||||
|
||||
|
||||
async def simple_chat(model: LLM, history: list) -> str:
|
||||
"""使用 LLM 进行简单的单轮对话。
|
||||
|
||||
Args:
|
||||
model: LLM 实例。
|
||||
history: 构造好的历史记录(包含 system_prompt 和 user_prompt)。
|
||||
|
||||
Returns:
|
||||
模型的响应文本。
|
||||
"""
|
||||
kwargs = {
|
||||
"api_key": model.api_key,
|
||||
"model": model.model,
|
||||
"messages": history,
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
if model.base_url:
|
||||
kwargs["base_url"] = model.base_url
|
||||
|
||||
response = await acompletion(**kwargs)
|
||||
|
||||
return response.choices[0].message.content
|
||||
"""使用 LLM 进行简单的单轮对话。"""
|
||||
response = await model.provider.call(
|
||||
messages=history,
|
||||
model=model.model, # type: ignore[arg-type]
|
||||
api_key=model.api_key, # type: ignore[arg-type]
|
||||
base_url=model.base_url,
|
||||
)
|
||||
return response.content or ""
|
||||
|
||||
@@ -6,6 +6,7 @@ from app.core.llm.llm import LLM
|
||||
|
||||
class LLMFactory:
|
||||
"""LLM 工厂类,根据配置创建协调者、建模手、代码手和写作手的 LLM 实例。"""
|
||||
|
||||
task_id: str
|
||||
|
||||
def __init__(self, task_id: str) -> None:
|
||||
@@ -18,6 +19,7 @@ class LLMFactory:
|
||||
包含 (coordinator_llm, modeler_llm, coder_llm, writer_llm) 的元组。
|
||||
"""
|
||||
coordinator_llm = LLM(
|
||||
api_type=settings.COORDINATOR_API_TYPE,
|
||||
api_key=settings.COORDINATOR_API_KEY,
|
||||
model=settings.COORDINATOR_MODEL,
|
||||
base_url=settings.COORDINATOR_BASE_URL,
|
||||
@@ -26,6 +28,7 @@ class LLMFactory:
|
||||
)
|
||||
|
||||
modeler_llm = LLM(
|
||||
api_type=settings.MODELER_API_TYPE,
|
||||
api_key=settings.MODELER_API_KEY,
|
||||
model=settings.MODELER_MODEL,
|
||||
base_url=settings.MODELER_BASE_URL,
|
||||
@@ -34,6 +37,7 @@ class LLMFactory:
|
||||
)
|
||||
|
||||
coder_llm = LLM(
|
||||
api_type=settings.CODER_API_TYPE,
|
||||
api_key=settings.CODER_API_KEY,
|
||||
model=settings.CODER_MODEL,
|
||||
base_url=settings.CODER_BASE_URL,
|
||||
@@ -42,6 +46,7 @@ class LLMFactory:
|
||||
)
|
||||
|
||||
writer_llm = LLM(
|
||||
api_type=settings.WRITER_API_TYPE,
|
||||
api_key=settings.WRITER_API_KEY,
|
||||
model=settings.WRITER_MODEL,
|
||||
base_url=settings.WRITER_BASE_URL,
|
||||
@@ -50,52 +55,3 @@ class LLMFactory:
|
||||
)
|
||||
|
||||
return coordinator_llm, modeler_llm, coder_llm, writer_llm
|
||||
|
||||
def get_fallback_llms(self) -> dict[str, LLM | None]:
|
||||
"""创建所有 Agent 的 Fallback LLM 实例。
|
||||
|
||||
仅当对应 FALLBACK_*_API_KEY 和 FALLBACK_*_MODEL 均配置时才创建实例,
|
||||
否则返回 None,表示该 Agent 无 fallback。
|
||||
|
||||
Returns:
|
||||
以 Agent 名称为键、LLM 实例或 None 为值的字典。
|
||||
"""
|
||||
fallbacks: dict[str, LLM | None] = {}
|
||||
|
||||
agent_configs = [
|
||||
("coordinator", settings.FALLBACK_COORDINATOR_API_KEY, settings.FALLBACK_COORDINATOR_MODEL, settings.FALLBACK_COORDINATOR_BASE_URL, settings.FALLBACK_COORDINATOR_MAX_TOKENS),
|
||||
("modeler", settings.FALLBACK_MODELER_API_KEY, settings.FALLBACK_MODELER_MODEL, settings.FALLBACK_MODELER_BASE_URL, settings.FALLBACK_MODELER_MAX_TOKENS),
|
||||
("coder", settings.FALLBACK_CODER_API_KEY, settings.FALLBACK_CODER_MODEL, settings.FALLBACK_CODER_BASE_URL, settings.FALLBACK_CODER_MAX_TOKENS),
|
||||
("writer", settings.FALLBACK_WRITER_API_KEY, settings.FALLBACK_WRITER_MODEL, settings.FALLBACK_WRITER_BASE_URL, settings.FALLBACK_WRITER_MAX_TOKENS),
|
||||
]
|
||||
|
||||
for name, api_key, model, base_url, max_tokens in agent_configs:
|
||||
if api_key and model:
|
||||
fallbacks[name] = LLM(
|
||||
api_key=api_key,
|
||||
model=model,
|
||||
base_url=base_url,
|
||||
task_id=self.task_id,
|
||||
max_tokens=max_tokens,
|
||||
)
|
||||
else:
|
||||
fallbacks[name] = None
|
||||
|
||||
return fallbacks
|
||||
|
||||
def get_evaluator_llm(self) -> LLM | None:
|
||||
"""创建评估器的 LLM 实例。
|
||||
|
||||
仅当 EVALUATOR_API_KEY 和 EVALUATOR_MODEL 均配置时才创建实例。
|
||||
|
||||
Returns:
|
||||
LLM 实例,未配置时返回 None。
|
||||
"""
|
||||
if settings.EVALUATOR_API_KEY and settings.EVALUATOR_MODEL:
|
||||
return LLM(
|
||||
api_key=settings.EVALUATOR_API_KEY,
|
||||
model=settings.EVALUATOR_MODEL,
|
||||
base_url=settings.EVALUATOR_BASE_URL,
|
||||
task_id=self.task_id,
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"""LLM Provider 实现。"""
|
||||
|
||||
from app.core.llm.providers.base import BaseProvider
|
||||
from app.core.llm.providers.openai_chat import OpenAIChatProvider
|
||||
from app.core.llm.providers.openai_responses import OpenAIResponsesProvider
|
||||
from app.core.llm.providers.anthropic import AnthropicProvider
|
||||
|
||||
__all__ = [
|
||||
"BaseProvider",
|
||||
"OpenAIChatProvider",
|
||||
"OpenAIResponsesProvider",
|
||||
"AnthropicProvider",
|
||||
]
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Anthropic Messages API Provider。"""
|
||||
|
||||
import json as _json
|
||||
from anthropic import AsyncAnthropic
|
||||
from app.core.llm.providers.base import BaseProvider
|
||||
from app.core.llm.types import StandardResponse, ToolCall, Usage
|
||||
|
||||
|
||||
class AnthropicProvider(BaseProvider):
|
||||
"""Anthropic Messages API (/v1/messages) 实现。"""
|
||||
|
||||
async def call(
|
||||
self,
|
||||
messages: list[dict],
|
||||
model: str,
|
||||
api_key: str,
|
||||
base_url: str | None = None,
|
||||
tools: list[dict] | None = None,
|
||||
tool_choice: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
top_p: float | None = None,
|
||||
) -> StandardResponse:
|
||||
client = AsyncAnthropic(api_key=api_key, base_url=base_url)
|
||||
|
||||
system_prompt, anthropic_messages = self._convert_messages(messages)
|
||||
|
||||
kwargs: dict = {
|
||||
"model": model,
|
||||
"messages": anthropic_messages,
|
||||
"max_tokens": max_tokens or 4096,
|
||||
}
|
||||
if system_prompt:
|
||||
kwargs["system"] = system_prompt
|
||||
if top_p is not None:
|
||||
kwargs["top_p"] = top_p
|
||||
if tools:
|
||||
kwargs["tools"] = self._convert_tools(tools)
|
||||
if tool_choice:
|
||||
kwargs["tool_choice"] = self._convert_tool_choice(tool_choice)
|
||||
|
||||
response = await client.messages.create(**kwargs)
|
||||
|
||||
content_parts: list[str] = []
|
||||
tool_calls: list[ToolCall] = []
|
||||
|
||||
for block in response.content:
|
||||
if block.type == "text":
|
||||
content_parts.append(block.text)
|
||||
elif block.type == "tool_use":
|
||||
tool_calls.append(ToolCall(
|
||||
id=block.id,
|
||||
name=block.name,
|
||||
arguments=_json.dumps(block.input),
|
||||
))
|
||||
|
||||
content = "".join(content_parts) if content_parts else None
|
||||
|
||||
usage = Usage(
|
||||
prompt_tokens=response.usage.input_tokens,
|
||||
completion_tokens=response.usage.output_tokens,
|
||||
)
|
||||
|
||||
return StandardResponse(content=content, tool_calls=tool_calls, usage=usage)
|
||||
|
||||
def _convert_messages(self, messages: list[dict]) -> tuple[str | None, list[dict]]:
|
||||
"""将 OpenAI 格式 messages 转为 Anthropic 格式。"""
|
||||
system_prompt = None
|
||||
converted: list[dict] = []
|
||||
|
||||
for msg in messages:
|
||||
role = msg.get("role", "user")
|
||||
|
||||
if role == "system" and system_prompt is None:
|
||||
system_prompt = msg["content"]
|
||||
continue
|
||||
|
||||
if role == "assistant" and "tool_calls" in msg and msg["tool_calls"]:
|
||||
content_blocks: list[dict] = []
|
||||
if msg.get("content"):
|
||||
content_blocks.append({"type": "text", "text": msg["content"]})
|
||||
for tc in msg["tool_calls"]:
|
||||
content_blocks.append({
|
||||
"type": "tool_use",
|
||||
"id": tc["id"],
|
||||
"name": tc["function"]["name"],
|
||||
"input": _json.loads(tc["function"]["arguments"]),
|
||||
})
|
||||
converted.append({"role": "assistant", "content": content_blocks})
|
||||
continue
|
||||
|
||||
if role == "tool":
|
||||
converted.append({
|
||||
"role": "user",
|
||||
"content": [{
|
||||
"type": "tool_result",
|
||||
"tool_use_id": msg.get("tool_call_id", ""),
|
||||
"content": msg.get("content", ""),
|
||||
}],
|
||||
})
|
||||
continue
|
||||
|
||||
converted.append(msg)
|
||||
|
||||
return system_prompt, converted
|
||||
|
||||
def _convert_tools(self, tools: list[dict]) -> list[dict]:
|
||||
"""将 OpenAI tools 格式转为 Anthropic 格式。"""
|
||||
converted = []
|
||||
for tool in tools:
|
||||
if tool.get("type") == "function":
|
||||
func = tool["function"]
|
||||
converted.append({
|
||||
"name": func["name"],
|
||||
"description": func.get("description", ""),
|
||||
"input_schema": func.get("parameters", {}),
|
||||
})
|
||||
return converted
|
||||
|
||||
def _convert_tool_choice(self, tool_choice: str) -> dict:
|
||||
"""转换 tool_choice 为 Anthropic 格式。"""
|
||||
if tool_choice == "auto":
|
||||
return {"type": "auto"}
|
||||
if tool_choice == "none":
|
||||
return {"type": "none"}
|
||||
if tool_choice == "required":
|
||||
return {"type": "any"}
|
||||
return {"type": "auto"}
|
||||
@@ -0,0 +1,37 @@
|
||||
"""LLM Provider 抽象基类。"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from app.core.llm.types import StandardResponse
|
||||
|
||||
|
||||
class BaseProvider(ABC):
|
||||
"""LLM Provider 基类,定义统一的调用接口。"""
|
||||
|
||||
@abstractmethod
|
||||
async def call(
|
||||
self,
|
||||
messages: list[dict],
|
||||
model: str,
|
||||
api_key: str,
|
||||
base_url: str | None = None,
|
||||
tools: list[dict] | None = None,
|
||||
tool_choice: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
top_p: float | None = None,
|
||||
) -> StandardResponse:
|
||||
"""调用 LLM 并返回标准化响应。
|
||||
|
||||
Args:
|
||||
messages: 消息历史(OpenAI 格式)。
|
||||
model: 模型 ID。
|
||||
api_key: API 密钥。
|
||||
base_url: API 基础 URL。
|
||||
tools: 工具定义列表(OpenAI 格式)。
|
||||
tool_choice: 工具选择策略。
|
||||
max_tokens: 最大生成 token 数。
|
||||
top_p: 采样温度参数。
|
||||
|
||||
Returns:
|
||||
标准化响应。
|
||||
"""
|
||||
...
|
||||
@@ -0,0 +1,52 @@
|
||||
"""OpenAI Chat Completions API Provider。"""
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from app.core.llm.providers.base import BaseProvider
|
||||
from app.core.llm.types import StandardResponse, ToolCall, Usage
|
||||
|
||||
|
||||
class OpenAIChatProvider(BaseProvider):
|
||||
"""OpenAI Chat Completions API (/v1/chat/completions) 实现。"""
|
||||
|
||||
async def call(
|
||||
self,
|
||||
messages: list[dict],
|
||||
model: str,
|
||||
api_key: str,
|
||||
base_url: str | None = None,
|
||||
tools: list[dict] | None = None,
|
||||
tool_choice: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
top_p: float | None = None,
|
||||
) -> StandardResponse:
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
|
||||
kwargs: dict = {"model": model, "messages": messages}
|
||||
if max_tokens:
|
||||
kwargs["max_tokens"] = max_tokens
|
||||
if top_p is not None:
|
||||
kwargs["top_p"] = top_p
|
||||
if tools:
|
||||
kwargs["tools"] = tools
|
||||
if tool_choice:
|
||||
kwargs["tool_choice"] = tool_choice
|
||||
|
||||
response = await client.chat.completions.create(**kwargs)
|
||||
|
||||
choice = response.choices[0]
|
||||
message = choice.message
|
||||
|
||||
tool_calls: list[ToolCall] = []
|
||||
for tc in message.tool_calls or []:
|
||||
tool_calls.append(ToolCall(
|
||||
id=tc.id,
|
||||
name=tc.function.name,
|
||||
arguments=tc.function.arguments,
|
||||
))
|
||||
|
||||
usage = Usage(
|
||||
prompt_tokens=response.usage.prompt_tokens if response.usage else 0,
|
||||
completion_tokens=response.usage.completion_tokens if response.usage else 0,
|
||||
)
|
||||
|
||||
return StandardResponse(content=message.content, tool_calls=tool_calls, usage=usage)
|
||||
@@ -0,0 +1,112 @@
|
||||
"""OpenAI Responses API Provider。"""
|
||||
|
||||
from openai import AsyncOpenAI
|
||||
from app.core.llm.providers.base import BaseProvider
|
||||
from app.core.llm.types import StandardResponse, ToolCall, Usage
|
||||
|
||||
|
||||
class OpenAIResponsesProvider(BaseProvider):
|
||||
"""OpenAI Responses API (/v1/responses) 实现。"""
|
||||
|
||||
async def call(
|
||||
self,
|
||||
messages: list[dict],
|
||||
model: str,
|
||||
api_key: str,
|
||||
base_url: str | None = None,
|
||||
tools: list[dict] | None = None,
|
||||
tool_choice: str | None = None,
|
||||
max_tokens: int | None = None,
|
||||
top_p: float | None = None,
|
||||
) -> StandardResponse:
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
|
||||
input_items = self._messages_to_input(messages)
|
||||
|
||||
kwargs: dict = {"model": model, "input": input_items}
|
||||
if max_tokens:
|
||||
kwargs["max_output_tokens"] = max_tokens
|
||||
if top_p is not None:
|
||||
kwargs["top_p"] = top_p
|
||||
if tools:
|
||||
kwargs["tools"] = self._convert_tools(tools)
|
||||
if tool_choice:
|
||||
kwargs["tool_choice"] = self._convert_tool_choice(tool_choice)
|
||||
|
||||
response = await client.responses.create(**kwargs)
|
||||
|
||||
content_parts: list[str] = []
|
||||
tool_calls: list[ToolCall] = []
|
||||
|
||||
for item in response.output:
|
||||
if item.type == "message":
|
||||
for part in item.content:
|
||||
if part.type == "output_text":
|
||||
content_parts.append(part.text)
|
||||
elif item.type == "function_call":
|
||||
tool_calls.append(ToolCall(
|
||||
id=item.call_id,
|
||||
name=item.name,
|
||||
arguments=item.arguments,
|
||||
))
|
||||
|
||||
content = "".join(content_parts) if content_parts else None
|
||||
|
||||
usage = Usage(
|
||||
prompt_tokens=response.usage.input_tokens if response.usage else 0,
|
||||
completion_tokens=response.usage.output_tokens if response.usage else 0,
|
||||
)
|
||||
|
||||
return StandardResponse(content=content, tool_calls=tool_calls, usage=usage)
|
||||
|
||||
def _messages_to_input(self, messages: list[dict]) -> list[dict]:
|
||||
"""将 Chat Completions messages 格式转为 Responses input 格式。"""
|
||||
input_items = []
|
||||
for msg in messages:
|
||||
role = msg.get("role", "user")
|
||||
if role == "system":
|
||||
input_items.append({"role": "developer", "content": msg["content"]})
|
||||
elif role == "tool":
|
||||
input_items.append({
|
||||
"type": "function_call_output",
|
||||
"call_id": msg.get("tool_call_id", ""),
|
||||
"output": msg.get("content", ""),
|
||||
})
|
||||
elif role == "assistant" and "tool_calls" in msg:
|
||||
for tc in msg["tool_calls"]:
|
||||
input_items.append({
|
||||
"type": "function_call",
|
||||
"call_id": tc["id"],
|
||||
"name": tc["function"]["name"],
|
||||
"arguments": tc["function"]["arguments"],
|
||||
})
|
||||
if msg.get("content"):
|
||||
input_items.append({"role": "assistant", "content": msg["content"]})
|
||||
else:
|
||||
input_items.append({"role": role, "content": msg.get("content", "")})
|
||||
return input_items
|
||||
|
||||
def _convert_tools(self, tools: list[dict]) -> list[dict]:
|
||||
"""将 Chat Completions tools 格式转为 Responses tools 格式。"""
|
||||
converted = []
|
||||
for tool in tools:
|
||||
if tool.get("type") == "function":
|
||||
func = tool["function"]
|
||||
converted.append({
|
||||
"type": "function",
|
||||
"name": func["name"],
|
||||
"description": func.get("description", ""),
|
||||
"parameters": func.get("parameters", {}),
|
||||
"strict": func.get("strict", True),
|
||||
})
|
||||
return converted
|
||||
|
||||
def _convert_tool_choice(self, tool_choice: str) -> str | dict:
|
||||
"""转换 tool_choice 格式。"""
|
||||
if tool_choice == "auto":
|
||||
return "auto"
|
||||
if tool_choice == "none":
|
||||
return "none"
|
||||
if tool_choice == "required":
|
||||
return {"type": "function"}
|
||||
return tool_choice
|
||||
@@ -0,0 +1,32 @@
|
||||
"""LLM 响应标准化类型定义。"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolCall:
|
||||
"""标准化工具调用。"""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
arguments: str # JSON string
|
||||
|
||||
|
||||
@dataclass
|
||||
class Usage:
|
||||
"""Token 用量。"""
|
||||
|
||||
prompt_tokens: int = 0
|
||||
completion_tokens: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class StandardResponse:
|
||||
"""LLM 响应的标准化格式。
|
||||
|
||||
Agent 侧统一使用此格式访问 LLM 结果,不感知底层 API 差异。
|
||||
"""
|
||||
|
||||
content: str | None = None
|
||||
tool_calls: list[ToolCall] = field(default_factory=list)
|
||||
usage: Usage = field(default_factory=Usage)
|
||||
@@ -73,9 +73,10 @@ df['\\u5a74\\u513f\\u884c\\u4e3a\\u7279\\u5f81'] # No unicode escapes
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
import seaborn as sns
|
||||
sns.set_theme(style='ticks')
|
||||
|
||||
plt.rcParams.update({{
|
||||
'font.family': 'Arial',
|
||||
'font.family': 'sans-serif',
|
||||
'font.size': 11,
|
||||
'axes.titlesize': 12,
|
||||
'axes.titleweight': 'bold',
|
||||
@@ -92,9 +93,8 @@ plt.rcParams.update({{
|
||||
'savefig.bbox': 'tight',
|
||||
'savefig.pad_inches': 0.1,
|
||||
}})
|
||||
plt.rcParams['font.sans-serif'] = ['Noto Sans CJK JP']
|
||||
plt.rcParams['font.sans-serif'] = ['SimHei', 'Noto Sans CJK SC', 'Noto Sans SC', 'DejaVu Sans']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
sns.set_theme(style='ticks')
|
||||
|
||||
COLORS = {{
|
||||
'primary': '#2E5B88',
|
||||
@@ -128,7 +128,6 @@ FIG_SQUARE = (6, 6)
|
||||
- 低分辨率 PNG(用 300dpi,保存为 PNG 即可)
|
||||
|
||||
## 必须遵守
|
||||
- 只要用 matplotlib / seaborn 画中文图,必须先设置 `plt.rcParams['font.sans-serif'] = ['Noto Sans CJK JP']` 和 `plt.rcParams['axes.unicode_minus'] = False`
|
||||
- 去掉上右边框(已通过全局配置实现)
|
||||
- 使用统一的 COLORS 配色方案
|
||||
- 折线图用 `fill_between` 添加置信带
|
||||
|
||||
@@ -122,15 +122,6 @@ EDA阶段应涵盖:
|
||||
|
||||
---
|
||||
|
||||
# 知识库参考
|
||||
|
||||
如果系统提供了【知识库参考】内容,你应该:
|
||||
- 优先参考知识库中的建模方法和优秀论文方案
|
||||
- 结合题目特点选择最适合的方法
|
||||
- 在方案中注明参考的方法来源
|
||||
|
||||
---
|
||||
|
||||
# 输出规范
|
||||
|
||||
以 JSON 的形式输出,需遵守以下格式:
|
||||
|
||||
+30
-644
@@ -1,10 +1,8 @@
|
||||
"""工作流模块,编排多 Agent 协作完成数学建模任务。"""
|
||||
|
||||
from app.core.agents import WriterAgent, CoderAgent, CoordinatorAgent, ModelerAgent
|
||||
from app.core.agents.agent import Agent
|
||||
from app.schemas.request import Problem
|
||||
from app.schemas.response import SystemMessage
|
||||
from app.schemas.A2A import CoderToWriter, WriterResponse
|
||||
from app.tools.openalex_scholar import OpenAlexScholar
|
||||
from app.utils.log_util import logger
|
||||
from app.utils.common_utils import create_work_dir, get_config_template
|
||||
@@ -14,34 +12,7 @@ from app.tools.interpreter_factory import create_interpreter
|
||||
from app.services.redis_manager import redis_manager
|
||||
from app.tools.notebook_serializer import NotebookSerializer
|
||||
from app.core.flows import Flows
|
||||
from app.core.llm.llm import LLM
|
||||
from app.core.llm.llm_factory import LLMFactory
|
||||
from app.tools.base_interpreter import BaseCodeInterpreter
|
||||
from app.core.evaluator import Evaluator
|
||||
from app.core.functions import coder_tools, search_web_tool, search_knowledge_tool
|
||||
from app.tools.tool_registry import tool_registry
|
||||
from app.tools.web_searcher import WebSearcher
|
||||
from app.tools.knowledge_retriever import knowledge_retriever
|
||||
from app.services.checkpoint_manager import checkpoint_manager
|
||||
|
||||
|
||||
# CoderToWriter 中表示任务失败的前缀
|
||||
_CODER_FAILURE_PREFIX = "任务失败"
|
||||
|
||||
|
||||
def _is_coder_failed(response: CoderToWriter) -> bool:
|
||||
"""判断 CoderToWriter 响应是否表示任务失败。
|
||||
|
||||
Args:
|
||||
response: 代码手的响应。
|
||||
|
||||
Returns:
|
||||
True 如果响应内容以失败前缀开头。
|
||||
"""
|
||||
return bool(
|
||||
response.code_response
|
||||
and response.code_response.startswith(_CODER_FAILURE_PREFIX)
|
||||
)
|
||||
|
||||
|
||||
class WorkFlow:
|
||||
@@ -59,312 +30,11 @@ class WorkFlow:
|
||||
|
||||
class MathModelWorkFlow(WorkFlow):
|
||||
"""数学建模工作流,协调协调者、建模手、代码手和写作手完成完整建模任务。"""
|
||||
|
||||
task_id: str #
|
||||
work_dir: str # worklow work dir
|
||||
ques_count: int = 0 # 问题数量
|
||||
questions: dict[str, str | int] = {} # 问题
|
||||
|
||||
async def _run_with_handoff(
|
||||
self,
|
||||
agent: Agent,
|
||||
method_name: str,
|
||||
args: tuple,
|
||||
agent_label: str,
|
||||
fallback_llm: LLM | None,
|
||||
agent_cls: type,
|
||||
agent_init_kwargs: dict | None = None,
|
||||
):
|
||||
"""通用 Hand Off 包装器:主 Agent 失败时新建 Fallback Agent 重试。
|
||||
|
||||
Args:
|
||||
agent: 主 Agent 实例。
|
||||
method_name: 要调用的方法名(如 "run")。
|
||||
args: 方法参数元组。
|
||||
agent_label: 用于日志和消息的 Agent 标签。
|
||||
fallback_llm: Fallback LLM 实例,None 表示无 fallback。
|
||||
agent_cls: Agent 类,用于创建 fallback 实例。
|
||||
agent_init_kwargs: 创建 fallback Agent 时的额外关键字参数。
|
||||
|
||||
Returns:
|
||||
方法调用的返回值。
|
||||
"""
|
||||
try:
|
||||
method = getattr(agent, method_name)
|
||||
return await method(*args)
|
||||
except Exception as e:
|
||||
logger.error(f"{agent_label} 主 LLM 执行失败: {e}")
|
||||
if fallback_llm:
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content=f"{agent_label}主模型失败({type(e).__name__}),正在切换备用模型...",
|
||||
type="warning",
|
||||
),
|
||||
)
|
||||
# 新建 Agent 实例,避免脏 chat_history
|
||||
init_kwargs = agent_init_kwargs or {}
|
||||
fallback_agent = agent_cls(self.task_id, fallback_llm, **init_kwargs)
|
||||
try:
|
||||
method = getattr(fallback_agent, method_name)
|
||||
return await method(*args)
|
||||
except Exception as fallback_e:
|
||||
logger.error(f"{agent_label} Fallback 也失败: {fallback_e}")
|
||||
raise fallback_e
|
||||
else:
|
||||
raise e
|
||||
|
||||
async def _run_coder_with_handoff(
|
||||
self,
|
||||
coder_agent: CoderAgent,
|
||||
prompt: str,
|
||||
subtask_title: str,
|
||||
fallback_llm: LLM | None,
|
||||
code_interpreter: BaseCodeInterpreter,
|
||||
work_dir: str,
|
||||
) -> CoderToWriter:
|
||||
"""Coder 专用 Hand Off:主 Agent 返回失败响应或抛异常时切换 Fallback。
|
||||
|
||||
Args:
|
||||
coder_agent: 主 CoderAgent 实例。
|
||||
prompt: 子任务提示。
|
||||
subtask_title: 子任务标题。
|
||||
fallback_llm: Fallback LLM 实例。
|
||||
code_interpreter: 代码解释器。
|
||||
work_dir: 工作目录。
|
||||
|
||||
Returns:
|
||||
CoderToWriter 响应。
|
||||
"""
|
||||
try:
|
||||
response = await coder_agent.run(prompt=prompt, subtask_title=subtask_title)
|
||||
if _is_coder_failed(response) and fallback_llm:
|
||||
# 主 Agent 返回失败响应,尝试 Fallback
|
||||
logger.warning("CoderAgent 主 LLM 返回失败响应,尝试 Fallback")
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content="代码手主模型失败,正在切换备用模型...",
|
||||
type="warning",
|
||||
),
|
||||
)
|
||||
fallback_agent = CoderAgent(
|
||||
task_id=self.task_id,
|
||||
model=fallback_llm,
|
||||
work_dir=work_dir,
|
||||
max_retries=settings.MAX_RETRIES,
|
||||
code_interpreter=code_interpreter,
|
||||
)
|
||||
return await fallback_agent.run(
|
||||
prompt=prompt, subtask_title=subtask_title
|
||||
)
|
||||
return response
|
||||
except Exception as e:
|
||||
logger.error(f"CoderAgent 主 LLM 执行异常: {e}")
|
||||
if fallback_llm:
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content=f"代码手主模型异常({type(e).__name__}),正在切换备用模型...",
|
||||
type="warning",
|
||||
),
|
||||
)
|
||||
fallback_agent = CoderAgent(
|
||||
task_id=self.task_id,
|
||||
model=fallback_llm,
|
||||
work_dir=work_dir,
|
||||
max_retries=settings.MAX_RETRIES,
|
||||
code_interpreter=code_interpreter,
|
||||
)
|
||||
try:
|
||||
return await fallback_agent.run(
|
||||
prompt=prompt, subtask_title=subtask_title
|
||||
)
|
||||
except Exception as fallback_e:
|
||||
logger.error(f"CoderAgent Fallback 也失败: {fallback_e}")
|
||||
raise fallback_e
|
||||
else:
|
||||
raise e
|
||||
|
||||
async def _run_writer_with_feedback(
|
||||
self,
|
||||
writer_agent: WriterAgent,
|
||||
prompt: str,
|
||||
sub_title: str,
|
||||
evaluator: Evaluator | None,
|
||||
fallback_llm: LLM | None,
|
||||
agent_init_kwargs: dict,
|
||||
) -> WriterResponse:
|
||||
"""写作手 Feedback Rerun:评估不通过时注入反馈重跑。
|
||||
|
||||
通过拼接反馈到 prompt 末尾实现,利用已有的 run(prompt) 接口。
|
||||
|
||||
Args:
|
||||
writer_agent: 写作手 Agent 实例。
|
||||
prompt: 原始写作提示。
|
||||
sub_title: 子任务标题。
|
||||
evaluator: 评估器实例,None 表示无评估。
|
||||
fallback_llm: Fallback LLM 实例。
|
||||
agent_init_kwargs: 创建 fallback Agent 时的额外参数。
|
||||
|
||||
Returns:
|
||||
WriterResponse 响应。
|
||||
"""
|
||||
current_prompt = prompt
|
||||
last_response = None
|
||||
|
||||
for feedback_round in range(settings.MAX_FEEDBACK_ROUNDS + 1):
|
||||
response = await self._run_with_handoff(
|
||||
agent=writer_agent,
|
||||
method_name="run",
|
||||
args=(current_prompt,),
|
||||
agent_label="写作手",
|
||||
fallback_llm=fallback_llm,
|
||||
agent_cls=WriterAgent,
|
||||
agent_init_kwargs=agent_init_kwargs,
|
||||
)
|
||||
last_response = response
|
||||
|
||||
# 最后一轮或无评估器,不再重跑
|
||||
if not evaluator or feedback_round == settings.MAX_FEEDBACK_ROUNDS:
|
||||
break
|
||||
|
||||
eval_result = await evaluator.evaluate(
|
||||
task_description=current_prompt,
|
||||
agent_output=str(response.response_content)[:2000],
|
||||
)
|
||||
logger.info(
|
||||
f"[Feedback] 写作手 {sub_title} 第{feedback_round + 1}轮评估: "
|
||||
f"score={eval_result.score:.2f}, passed={eval_result.passed}"
|
||||
)
|
||||
|
||||
if eval_result.passed or eval_result.score >= settings.EVALUATION_THRESHOLD:
|
||||
break
|
||||
|
||||
# 注入反馈到 prompt 末尾
|
||||
current_prompt = f"{prompt}\n\n【评估反馈(第{feedback_round + 1}轮)】\n{eval_result.feedback}"
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content=f"写作手 {sub_title} 评估未通过(score={eval_result.score:.2f}),注入反馈重跑...",
|
||||
type="warning",
|
||||
),
|
||||
)
|
||||
# 新建 Agent 实例以获得干净状态
|
||||
writer_agent = WriterAgent(
|
||||
self.task_id, writer_agent.model, **agent_init_kwargs
|
||||
)
|
||||
|
||||
return last_response # type: ignore[return-value]
|
||||
|
||||
async def _run_coder_with_feedback(
|
||||
self,
|
||||
coder_agent: CoderAgent,
|
||||
prompt: str,
|
||||
subtask_title: str,
|
||||
evaluator: Evaluator | None,
|
||||
fallback_llm: LLM | None,
|
||||
code_interpreter: BaseCodeInterpreter,
|
||||
work_dir: str,
|
||||
) -> CoderToWriter:
|
||||
"""代码手 Feedback Rerun:评估不通过时注入反馈重跑。
|
||||
|
||||
Args:
|
||||
coder_agent: 代码手 Agent 实例。
|
||||
prompt: 原始任务提示。
|
||||
subtask_title: 子任务标题。
|
||||
evaluator: 评估器实例。
|
||||
fallback_llm: Fallback LLM 实例。
|
||||
code_interpreter: 代码解释器。
|
||||
work_dir: 工作目录。
|
||||
|
||||
Returns:
|
||||
CoderToWriter 响应。
|
||||
"""
|
||||
current_prompt = prompt
|
||||
last_response = None
|
||||
|
||||
for feedback_round in range(settings.MAX_FEEDBACK_ROUNDS + 1):
|
||||
response = await self._run_coder_with_handoff(
|
||||
coder_agent=coder_agent,
|
||||
prompt=current_prompt,
|
||||
subtask_title=subtask_title,
|
||||
fallback_llm=fallback_llm,
|
||||
code_interpreter=code_interpreter,
|
||||
work_dir=work_dir,
|
||||
)
|
||||
last_response = response
|
||||
|
||||
if not evaluator or feedback_round == settings.MAX_FEEDBACK_ROUNDS:
|
||||
break
|
||||
|
||||
if _is_coder_failed(response):
|
||||
break
|
||||
|
||||
eval_result = await evaluator.evaluate(
|
||||
task_description=current_prompt,
|
||||
agent_output=response.code_response or "",
|
||||
)
|
||||
logger.info(
|
||||
f"[Feedback] 代码手 {subtask_title} 第{feedback_round + 1}轮评估: "
|
||||
f"score={eval_result.score:.2f}, passed={eval_result.passed}"
|
||||
)
|
||||
|
||||
if eval_result.passed or eval_result.score >= settings.EVALUATION_THRESHOLD:
|
||||
break
|
||||
|
||||
current_prompt = f"{prompt}\n\n【评估反馈(第{feedback_round + 1}轮)】\n{eval_result.feedback}"
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content=f"代码手 {subtask_title} 评估未通过(score={eval_result.score:.2f}),注入反馈重跑...",
|
||||
type="warning",
|
||||
),
|
||||
)
|
||||
coder_agent = CoderAgent(
|
||||
task_id=self.task_id,
|
||||
model=coder_agent.model,
|
||||
work_dir=work_dir,
|
||||
max_retries=settings.MAX_RETRIES,
|
||||
code_interpreter=code_interpreter,
|
||||
)
|
||||
|
||||
return last_response # type: ignore[return-value]
|
||||
|
||||
async def _handle_checkpoint(
|
||||
self,
|
||||
checkpoint_id: str,
|
||||
prompt: dict,
|
||||
) -> dict:
|
||||
"""处理 HIL 检查点,等待用户决策并返回。
|
||||
|
||||
Args:
|
||||
checkpoint_id: 检查点 ID。
|
||||
prompt: 审批提示内容。
|
||||
|
||||
Returns:
|
||||
用户决策字典,HIL 未启用时返回 {"action": "confirm"}。
|
||||
"""
|
||||
if not settings.HIL_ENABLED:
|
||||
return {"action": "confirm"}
|
||||
|
||||
checkpoints = settings.HIL_CHECKPOINTS
|
||||
# code_review 默认关闭,需显式开启
|
||||
if checkpoint_id.startswith("code_review") and not checkpoints.get(
|
||||
"code_review", False
|
||||
):
|
||||
return {"action": "confirm"}
|
||||
if not checkpoints.get(checkpoint_id, True):
|
||||
return {"action": "confirm"}
|
||||
|
||||
decision = await checkpoint_manager.wait_for_decision(
|
||||
task_id=self.task_id,
|
||||
checkpoint_id=checkpoint_id,
|
||||
prompt=prompt,
|
||||
timeout=settings.HIL_TIMEOUT,
|
||||
)
|
||||
return decision
|
||||
|
||||
async def execute(self, problem: Problem): # type: ignore[reportIncompatibleMethodOverride]
|
||||
"""执行数学建模工作流。
|
||||
|
||||
@@ -376,129 +46,25 @@ class MathModelWorkFlow(WorkFlow):
|
||||
|
||||
llm_factory = LLMFactory(self.task_id)
|
||||
coordinator_llm, modeler_llm, coder_llm, writer_llm = llm_factory.get_all_llms()
|
||||
fallback_llms = llm_factory.get_fallback_llms()
|
||||
|
||||
# 评估器(Shadow Mode:只记录不触发重跑)
|
||||
evaluator_llm = llm_factory.get_evaluator_llm()
|
||||
evaluator = Evaluator(evaluator_llm) if evaluator_llm else None
|
||||
|
||||
coordinator_agent = CoordinatorAgent(self.task_id, coordinator_llm)
|
||||
|
||||
# ---- Web Search 初始化 ----
|
||||
search_evidence: list = []
|
||||
active_coder_tools = list(coder_tools) # 复制默认工具列表
|
||||
modeler_tools: list[dict] | None = None
|
||||
|
||||
if settings.SEARCH_ENABLED and settings.TAVILY_API_KEY:
|
||||
web_searcher = WebSearcher(modeler_llm)
|
||||
|
||||
async def _search_handler(arguments: dict, task_id: str) -> str:
|
||||
"""search_web 工具 handler,同时收集 evidence。"""
|
||||
query = arguments.get("query", "")
|
||||
data_type = arguments.get("data_type", "general")
|
||||
max_results = arguments.get("max_results", 5)
|
||||
|
||||
evidence_list = await web_searcher.search(query, data_type, max_results)
|
||||
search_evidence.extend(evidence_list)
|
||||
|
||||
if not evidence_list:
|
||||
return "未搜索到相关数据"
|
||||
|
||||
result_parts = []
|
||||
for i, ev in enumerate(evidence_list, 1):
|
||||
part = f"[数据 {i}] {ev.content}"
|
||||
if ev.unit:
|
||||
part += f" (单位: {ev.unit})"
|
||||
if ev.time_range:
|
||||
part += f" (时间: {ev.time_range})"
|
||||
if ev.region:
|
||||
part += f" (地域: {ev.region})"
|
||||
if ev.source_url:
|
||||
part += f"\n 来源: {ev.source_url}"
|
||||
if ev.original_excerpt:
|
||||
part += f"\n 原文: {ev.original_excerpt[:200]}"
|
||||
result_parts.append(part)
|
||||
|
||||
return "\n\n".join(result_parts)
|
||||
|
||||
tool_registry.register("search_web", _search_handler, search_web_tool)
|
||||
active_coder_tools.append(search_web_tool)
|
||||
modeler_tools = [search_web_tool]
|
||||
|
||||
# ---- RAG 知识库初始化 ----
|
||||
if settings.RAG_ENABLED:
|
||||
active_coder_tools.append(search_knowledge_tool)
|
||||
if modeler_tools is not None:
|
||||
modeler_tools.append(search_knowledge_tool)
|
||||
|
||||
async def _knowledge_handler(arguments: dict, task_id: str) -> str:
|
||||
"""search_knowledge 工具 handler。"""
|
||||
query = arguments.get("query", "")
|
||||
scope = arguments.get("scope", "method")
|
||||
method_name = arguments.get("method_name", "") or None
|
||||
|
||||
# 根据 scope 映射 source_type
|
||||
scope_map = {
|
||||
"method": "textbook,paper",
|
||||
"code": "code",
|
||||
"paper": "paper,problem",
|
||||
}
|
||||
source_type = scope_map.get(scope, "textbook")
|
||||
|
||||
evidence_list = await knowledge_retriever.retrieve(
|
||||
query=query, source_type=source_type, method_name=method_name
|
||||
)
|
||||
if not evidence_list:
|
||||
return "知识库中未找到相关内容"
|
||||
|
||||
result_parts = []
|
||||
for i, ke in enumerate(evidence_list, 1):
|
||||
part = f"[知识 {i}] {ke.content[:400]}"
|
||||
if ke.method_name:
|
||||
part += f" (方法: {ke.method_name})"
|
||||
if ke.source_title:
|
||||
part += f" (来源: {ke.source_title})"
|
||||
result_parts.append(part)
|
||||
|
||||
return "\n\n".join(result_parts)
|
||||
|
||||
tool_registry.register(
|
||||
"search_knowledge", _knowledge_handler, search_knowledge_tool
|
||||
)
|
||||
coordinator_agent = CoordinatorAgent(
|
||||
self.task_id, coordinator_llm,
|
||||
context_window=settings.COORDINATOR_CONTEXT_WINDOW,
|
||||
)
|
||||
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(content="识别用户意图和拆解问题ing..."),
|
||||
)
|
||||
|
||||
# ---- Coordinator: 主 LLM → Fallback Hand Off ----
|
||||
coordinator_response = await self._run_with_handoff(
|
||||
agent=coordinator_agent,
|
||||
method_name="run",
|
||||
args=(problem.ques_all,),
|
||||
agent_label="协调者",
|
||||
fallback_llm=fallback_llms.get("coordinator"),
|
||||
agent_cls=CoordinatorAgent,
|
||||
)
|
||||
self.questions = coordinator_response.questions
|
||||
self.ques_count = coordinator_response.ques_count
|
||||
|
||||
# ---- HIL Checkpoint: problem_split ----
|
||||
hil_decision = await self._handle_checkpoint(
|
||||
"problem_split",
|
||||
{"step": "problem_split", "questions": coordinator_response.questions},
|
||||
)
|
||||
if hil_decision.get("action") == "abort":
|
||||
await redis_manager.publish_message(
|
||||
self.task_id, SystemMessage(content="用户中止任务", type="error")
|
||||
)
|
||||
return
|
||||
if hil_decision.get("action") == "edit":
|
||||
# 用用户修改后的问题继续
|
||||
coordinator_response.questions = hil_decision.get(
|
||||
"content", coordinator_response.questions
|
||||
)
|
||||
try:
|
||||
coordinator_response = await coordinator_agent.run(problem.ques_all)
|
||||
self.questions = coordinator_response.questions
|
||||
self.ques_count = coordinator_response.ques_count
|
||||
except Exception as e:
|
||||
# 非数学建模问题
|
||||
logger.error(f"CoordinatorAgent 执行失败: {e}")
|
||||
raise e
|
||||
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
@@ -510,104 +76,15 @@ class MathModelWorkFlow(WorkFlow):
|
||||
SystemMessage(content="建模手开始建模ing..."),
|
||||
)
|
||||
|
||||
# ---- RAG: 为建模手检索知识 ----
|
||||
modeler_knowledge_text = ""
|
||||
if settings.RAG_ENABLED:
|
||||
# 根据问题描述检索建模方法知识
|
||||
query_text = " ".join(
|
||||
str(v) for v in self.questions.values() if isinstance(v, str)
|
||||
)[:500]
|
||||
modeler_knowledge = await knowledge_retriever.retrieve(
|
||||
query=query_text, source_type="textbook,paper"
|
||||
)
|
||||
if modeler_knowledge:
|
||||
knowledge_lines = []
|
||||
for ke in modeler_knowledge:
|
||||
line = f"- {ke.content[:300]}"
|
||||
if ke.method_name:
|
||||
line += f" (方法: {ke.method_name})"
|
||||
if ke.source_title:
|
||||
line += f" (来源: {ke.source_title})"
|
||||
knowledge_lines.append(line)
|
||||
modeler_knowledge_text = (
|
||||
"\n【知识库参考】\n" + "\n".join(knowledge_lines) + "\n"
|
||||
)
|
||||
modeler_agent = ModelerAgent(
|
||||
self.task_id, modeler_llm,
|
||||
context_window=settings.MODELER_CONTEXT_WINDOW,
|
||||
)
|
||||
|
||||
# ---- Modeler: 主 LLM → Fallback Hand Off ----
|
||||
modeler_agent = ModelerAgent(self.task_id, modeler_llm)
|
||||
if modeler_tools:
|
||||
# 支持工具调用的建模手(单次 tool call 模式)
|
||||
try:
|
||||
# 注入知识库参考到 questions
|
||||
if modeler_knowledge_text:
|
||||
coordinator_response.questions["_knowledge_reference"] = (
|
||||
modeler_knowledge_text
|
||||
)
|
||||
modeler_response = await modeler_agent.run_with_tools(
|
||||
coordinator_response, tools=modeler_tools
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"建模手主 LLM 执行失败: {e}")
|
||||
fallback_llm = fallback_llms.get("modeler")
|
||||
if fallback_llm:
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(
|
||||
content=f"建模手主模型失败({type(e).__name__}),正在切换备用模型...",
|
||||
type="warning",
|
||||
),
|
||||
)
|
||||
fallback_agent = ModelerAgent(self.task_id, fallback_llm)
|
||||
modeler_response = await fallback_agent.run_with_tools(
|
||||
coordinator_response, tools=modeler_tools
|
||||
)
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
modeler_response = await self._run_with_handoff(
|
||||
agent=modeler_agent,
|
||||
method_name="run",
|
||||
args=(coordinator_response,),
|
||||
agent_label="建模手",
|
||||
fallback_llm=fallback_llms.get("modeler"),
|
||||
agent_cls=ModelerAgent,
|
||||
)
|
||||
modeler_response = await modeler_agent.run(coordinator_response)
|
||||
|
||||
user_output = UserOutput(work_dir=self.work_dir, ques_count=self.ques_count)
|
||||
|
||||
# ---- HIL Checkpoint: model_selection ----
|
||||
hil_decision = await self._handle_checkpoint(
|
||||
"model_selection",
|
||||
{
|
||||
"step": "model_selection",
|
||||
"solutions": modeler_response.questions_solution,
|
||||
},
|
||||
)
|
||||
if hil_decision.get("action") == "abort":
|
||||
await redis_manager.publish_message(
|
||||
self.task_id, SystemMessage(content="用户中止任务", type="error")
|
||||
)
|
||||
return
|
||||
if hil_decision.get("action") == "edit":
|
||||
modeler_response.questions_solution = hil_decision.get(
|
||||
"content", modeler_response.questions_solution
|
||||
)
|
||||
if hil_decision.get("action") == "regenerate":
|
||||
modeler_agent = ModelerAgent(self.task_id, modeler_llm)
|
||||
if modeler_tools:
|
||||
modeler_response = await modeler_agent.run_with_tools(
|
||||
coordinator_response, tools=modeler_tools
|
||||
)
|
||||
else:
|
||||
modeler_response = await self._run_with_handoff(
|
||||
agent=modeler_agent,
|
||||
method_name="run",
|
||||
args=(coordinator_response,),
|
||||
agent_label="建模手(重新生成)",
|
||||
fallback_llm=fallback_llms.get("modeler"),
|
||||
agent_cls=ModelerAgent,
|
||||
)
|
||||
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(content="正在创建代码沙盒环境"),
|
||||
@@ -621,9 +98,8 @@ class MathModelWorkFlow(WorkFlow):
|
||||
notebook_serializer=notebook_serializer,
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
|
||||
assert settings.OPENALEX_EMAIL is not None, "OPENALEX_EMAIL 未配置"
|
||||
assert settings.OPENALEX_API_KEY is not None, "OPENALEX_API_KEY 未配置"
|
||||
scholar = OpenAlexScholar(
|
||||
task_id=self.task_id,
|
||||
email=settings.OPENALEX_EMAIL,
|
||||
@@ -640,6 +116,7 @@ class MathModelWorkFlow(WorkFlow):
|
||||
SystemMessage(content="初始化代码手"),
|
||||
)
|
||||
|
||||
# modeler_agent
|
||||
coder_agent = CoderAgent(
|
||||
task_id=problem.task_id,
|
||||
model=coder_llm,
|
||||
@@ -647,9 +124,8 @@ class MathModelWorkFlow(WorkFlow):
|
||||
max_chat_turns=settings.MAX_CHAT_TURNS,
|
||||
max_retries=settings.MAX_RETRIES,
|
||||
code_interpreter=code_interpreter,
|
||||
context_window=settings.CODER_CONTEXT_WINDOW,
|
||||
)
|
||||
# 更新 coder 的可用工具列表(包含 search_web 等动态注册的工具)
|
||||
coder_agent.available_tools = active_coder_tools
|
||||
|
||||
writer_agent = WriterAgent(
|
||||
task_id=problem.task_id,
|
||||
@@ -657,51 +133,23 @@ class MathModelWorkFlow(WorkFlow):
|
||||
comp_template=problem.comp_template,
|
||||
format_output=problem.format_output,
|
||||
scholar=scholar,
|
||||
context_window=settings.WRITER_CONTEXT_WINDOW,
|
||||
)
|
||||
|
||||
flows = Flows(self.questions)
|
||||
|
||||
################################################ solution steps
|
||||
|
||||
# ---- RAG: 为代码手检索代码模板知识 ----
|
||||
coder_knowledge = None
|
||||
if settings.RAG_ENABLED:
|
||||
query_text = " ".join(
|
||||
str(v) for v in self.questions.values() if isinstance(v, str)
|
||||
)[:500]
|
||||
coder_knowledge = await knowledge_retriever.retrieve(
|
||||
query=query_text, source_type="code"
|
||||
)
|
||||
|
||||
solution_flows = flows.get_solution_flows(
|
||||
self.questions,
|
||||
modeler_response,
|
||||
search_evidence=search_evidence or None,
|
||||
coder_knowledge=coder_knowledge,
|
||||
)
|
||||
solution_flows = flows.get_solution_flows(self.questions, modeler_response)
|
||||
config_template = get_config_template(problem.comp_template)
|
||||
|
||||
writer_agent_init_kwargs = {
|
||||
"comp_template": problem.comp_template,
|
||||
"format_output": problem.format_output,
|
||||
"scholar": scholar,
|
||||
}
|
||||
|
||||
for key, value in solution_flows.items():
|
||||
await redis_manager.publish_message(
|
||||
self.task_id,
|
||||
SystemMessage(content=f"代码手开始求解{key}"),
|
||||
)
|
||||
|
||||
# ---- Coder: Feedback Rerun + Hand Off ----
|
||||
coder_response = await self._run_coder_with_feedback(
|
||||
coder_agent=coder_agent,
|
||||
prompt=value["coder_prompt"],
|
||||
subtask_title=key,
|
||||
evaluator=evaluator,
|
||||
fallback_llm=fallback_llms.get("coder"),
|
||||
code_interpreter=code_interpreter,
|
||||
work_dir=self.work_dir,
|
||||
coder_response = await coder_agent.run(
|
||||
prompt=value["coder_prompt"], subtask_title=key
|
||||
)
|
||||
|
||||
await redis_manager.publish_message(
|
||||
@@ -709,36 +157,8 @@ class MathModelWorkFlow(WorkFlow):
|
||||
SystemMessage(content=f"代码手求解成功{key}", type="success"),
|
||||
)
|
||||
|
||||
# ---- HIL Checkpoint: code_review_{key} ----
|
||||
hil_decision = await self._handle_checkpoint(
|
||||
f"code_review_{key}",
|
||||
{
|
||||
"step": f"code_review_{key}",
|
||||
"subtask": key,
|
||||
"code_response": coder_response.code_response or "",
|
||||
},
|
||||
)
|
||||
if hil_decision.get("action") == "abort":
|
||||
await redis_manager.publish_message(
|
||||
self.task_id, SystemMessage(content="用户中止任务", type="error")
|
||||
)
|
||||
return
|
||||
if hil_decision.get("action") == "regenerate":
|
||||
coder_response = await self._run_coder_with_feedback(
|
||||
coder_agent=coder_agent,
|
||||
prompt=value["coder_prompt"],
|
||||
subtask_title=key,
|
||||
evaluator=evaluator,
|
||||
fallback_llm=fallback_llms.get("coder"),
|
||||
code_interpreter=code_interpreter,
|
||||
work_dir=self.work_dir,
|
||||
)
|
||||
|
||||
writer_prompt = flows.get_writer_prompt(
|
||||
key,
|
||||
coder_response.code_response or "",
|
||||
code_interpreter,
|
||||
config_template,
|
||||
key, coder_response.code_response or "", code_interpreter, config_template
|
||||
)
|
||||
|
||||
await redis_manager.publish_message(
|
||||
@@ -746,14 +166,11 @@ class MathModelWorkFlow(WorkFlow):
|
||||
SystemMessage(content=f"论文手开始写{key}部分"),
|
||||
)
|
||||
|
||||
# ---- Writer: Feedback Rerun + Hand Off ----
|
||||
writer_response = await self._run_writer_with_feedback(
|
||||
writer_agent=writer_agent,
|
||||
prompt=writer_prompt,
|
||||
## TODO: 图片引用错误
|
||||
writer_response = await writer_agent.run(
|
||||
writer_prompt,
|
||||
available_images=coder_response.created_images,
|
||||
sub_title=key,
|
||||
evaluator=evaluator,
|
||||
fallback_llm=fallback_llms.get("writer"),
|
||||
agent_init_kwargs=writer_agent_init_kwargs,
|
||||
)
|
||||
|
||||
await redis_manager.publish_message(
|
||||
@@ -770,18 +187,8 @@ class MathModelWorkFlow(WorkFlow):
|
||||
|
||||
################################################ write steps
|
||||
|
||||
# ---- RAG: 为写作手检索论文写作模板知识 ----
|
||||
writer_knowledge = None
|
||||
if settings.RAG_ENABLED:
|
||||
writer_knowledge = await knowledge_retriever.retrieve(
|
||||
query=str(problem.ques_all)[:500], source_type="paper,problem"
|
||||
)
|
||||
|
||||
write_flows = flows.get_write_flows(
|
||||
user_output,
|
||||
config_template,
|
||||
problem.ques_all,
|
||||
writer_knowledge=writer_knowledge,
|
||||
user_output, config_template, problem.ques_all
|
||||
)
|
||||
for key, value in write_flows.items():
|
||||
await redis_manager.publish_message(
|
||||
@@ -789,31 +196,10 @@ class MathModelWorkFlow(WorkFlow):
|
||||
SystemMessage(content=f"论文手开始写{key}部分"),
|
||||
)
|
||||
|
||||
writer_response = await self._run_writer_with_feedback(
|
||||
writer_agent=writer_agent,
|
||||
prompt=value,
|
||||
sub_title=key,
|
||||
evaluator=evaluator,
|
||||
fallback_llm=fallback_llms.get("writer"),
|
||||
agent_init_kwargs=writer_agent_init_kwargs,
|
||||
)
|
||||
writer_response = await writer_agent.run(prompt=value, sub_title=key)
|
||||
|
||||
user_output.set_res(key, writer_response)
|
||||
|
||||
logger.info(user_output.get_res())
|
||||
|
||||
# ---- HIL Checkpoint: paper_review ----
|
||||
hil_decision = await self._handle_checkpoint(
|
||||
"paper_review",
|
||||
{
|
||||
"step": "paper_review",
|
||||
"summary": user_output.get_res(),
|
||||
},
|
||||
)
|
||||
if hil_decision.get("action") == "abort":
|
||||
await redis_manager.publish_message(
|
||||
self.task_id, SystemMessage(content="用户中止任务", type="error")
|
||||
)
|
||||
return
|
||||
|
||||
user_output.save_result()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from aiofile import async_open
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from app.config.setting import settings
|
||||
from app.utils.common_utils import ensure_safe_task_id, get_config_template
|
||||
@@ -31,7 +32,7 @@ def _require_safe_task_id(task_id: str) -> str:
|
||||
raise HTTPException(status_code=400, detail="非法任务ID") from exc
|
||||
|
||||
|
||||
def _load_task_messages_from_file(task_id: str) -> list[dict]:
|
||||
async def _load_task_messages_from_file(task_id: str) -> list[dict]:
|
||||
"""从文件加载指定任务的历史消息。
|
||||
|
||||
Args:
|
||||
@@ -46,8 +47,9 @@ def _load_task_messages_from_file(task_id: str) -> list[dict]:
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(message_file, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
async with async_open(message_file, "r", encoding="utf-8") as f:
|
||||
content = await f.read()
|
||||
data = json.loads(content)
|
||||
return data if isinstance(data, list) else []
|
||||
except Exception as e:
|
||||
logger.error(f"读取任务消息文件失败: {str(e)}")
|
||||
@@ -80,7 +82,7 @@ async def get_writer_seque():
|
||||
|
||||
@router.get("/messages")
|
||||
async def get_task_messages(task_id: str):
|
||||
return _load_task_messages_from_file(task_id)
|
||||
return await _load_task_messages_from_file(task_id)
|
||||
|
||||
|
||||
@router.get("/track")
|
||||
|
||||
@@ -19,8 +19,11 @@ from fastapi import HTTPException
|
||||
from icecream import ic # type: ignore[import-unresolved]
|
||||
from app.schemas.request import ExampleRequest
|
||||
from pydantic import BaseModel
|
||||
import litellm # type: ignore[import-unresolved]
|
||||
from app.config.setting import settings
|
||||
from app.config.setting import settings, ApiType
|
||||
from app.core.llm.providers.openai_chat import OpenAIChatProvider
|
||||
from app.core.llm.providers.openai_responses import OpenAIResponsesProvider
|
||||
from app.core.llm.providers.anthropic import AnthropicProvider
|
||||
from app.core.llm.providers.base import BaseProvider
|
||||
import requests
|
||||
|
||||
router = APIRouter()
|
||||
@@ -30,6 +33,7 @@ class ValidateApiKeyRequest(BaseModel):
|
||||
api_key: str
|
||||
base_url: str = "https://api.openai.com/v1"
|
||||
model_id: str
|
||||
api_type: str = "openai-chat"
|
||||
|
||||
|
||||
class ValidateOpenalexEmailRequest(BaseModel):
|
||||
@@ -65,21 +69,37 @@ async def save_api_config(request: SaveApiConfigRequest):
|
||||
settings.COORDINATOR_API_KEY = request.coordinator.get("apiKey", "")
|
||||
settings.COORDINATOR_MODEL = request.coordinator.get("modelId", "")
|
||||
settings.COORDINATOR_BASE_URL = request.coordinator.get("baseUrl", "")
|
||||
if api_type := request.coordinator.get("apiType"):
|
||||
settings.COORDINATOR_API_TYPE = api_type
|
||||
if cw := request.coordinator.get("contextWindow"):
|
||||
settings.COORDINATOR_CONTEXT_WINDOW = int(cw)
|
||||
|
||||
if request.modeler:
|
||||
settings.MODELER_API_KEY = request.modeler.get("apiKey", "")
|
||||
settings.MODELER_MODEL = request.modeler.get("modelId", "")
|
||||
settings.MODELER_BASE_URL = request.modeler.get("baseUrl", "")
|
||||
if api_type := request.modeler.get("apiType"):
|
||||
settings.MODELER_API_TYPE = api_type
|
||||
if cw := request.modeler.get("contextWindow"):
|
||||
settings.MODELER_CONTEXT_WINDOW = int(cw)
|
||||
|
||||
if request.coder:
|
||||
settings.CODER_API_KEY = request.coder.get("apiKey", "")
|
||||
settings.CODER_MODEL = request.coder.get("modelId", "")
|
||||
settings.CODER_BASE_URL = request.coder.get("baseUrl", "")
|
||||
if api_type := request.coder.get("apiType"):
|
||||
settings.CODER_API_TYPE = api_type
|
||||
if cw := request.coder.get("contextWindow"):
|
||||
settings.CODER_CONTEXT_WINDOW = int(cw)
|
||||
|
||||
if request.writer:
|
||||
settings.WRITER_API_KEY = request.writer.get("apiKey", "")
|
||||
settings.WRITER_MODEL = request.writer.get("modelId", "")
|
||||
settings.WRITER_BASE_URL = request.writer.get("baseUrl", "")
|
||||
if api_type := request.writer.get("apiType"):
|
||||
settings.WRITER_API_TYPE = api_type
|
||||
if cw := request.writer.get("contextWindow"):
|
||||
settings.WRITER_CONTEXT_WINDOW = int(cw)
|
||||
|
||||
if request.openalex_email:
|
||||
settings.OPENALEX_EMAIL = request.openalex_email
|
||||
@@ -96,15 +116,23 @@ async def validate_api_key(request: ValidateApiKeyRequest):
|
||||
验证 API Key 的有效性
|
||||
"""
|
||||
try:
|
||||
# 使用 litellm 发送测试请求
|
||||
await litellm.acompletion(
|
||||
model=request.model_id,
|
||||
provider: BaseProvider
|
||||
match request.api_type:
|
||||
case ApiType.OPENAI_RESPONSES:
|
||||
provider = OpenAIResponsesProvider()
|
||||
case ApiType.ANTHROPIC:
|
||||
provider = AnthropicProvider()
|
||||
case _:
|
||||
provider = OpenAIChatProvider()
|
||||
|
||||
await provider.call(
|
||||
messages=[{"role": "user", "content": "Hi"}],
|
||||
max_tokens=1,
|
||||
model=request.model_id,
|
||||
api_key=request.api_key,
|
||||
base_url=request.base_url
|
||||
if request.base_url != "https://api.openai.com/v1"
|
||||
else None,
|
||||
max_tokens=1,
|
||||
)
|
||||
|
||||
return ValidateApiKeyResponse(valid=True, message="✓ 模型 API 验证成功")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""WebSocket 路由模块,提供双向实时任务消息推送。"""
|
||||
"""WebSocket 路由模块,提供实时任务消息推送。"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
@@ -8,7 +8,6 @@ from starlette.websockets import WebSocketState
|
||||
|
||||
from app.schemas.response import SystemMessage
|
||||
from app.services.redis_manager import redis_manager
|
||||
from app.services.state_store import state_store
|
||||
from app.services.ws_manager import ws_manager
|
||||
from app.utils.common_utils import ensure_safe_task_id
|
||||
from app.utils.log_util import logger
|
||||
@@ -26,141 +25,11 @@ def _is_websocket_closed(websocket: WebSocket) -> bool:
|
||||
def _is_closed_send_error(error: Exception) -> bool:
|
||||
text = str(error)
|
||||
return (
|
||||
'Cannot call "send" once a close message has been sent' in text
|
||||
"Cannot call \"send\" once a close message has been sent" in text
|
||||
or "Unexpected ASGI message 'websocket.send'" in text
|
||||
)
|
||||
|
||||
|
||||
async def _handle_client_message(task_id: str, data: dict) -> None:
|
||||
"""处理客户端发来的消息。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
data: 客户端消息字典。
|
||||
"""
|
||||
msg_type = data.get("type")
|
||||
|
||||
if msg_type == "user_decision":
|
||||
# HIL 审批决策
|
||||
checkpoint_id = data.get("checkpoint_id", "")
|
||||
decision = data.get("decision", {})
|
||||
if checkpoint_id and decision:
|
||||
await state_store.set(
|
||||
namespace="checkpoint",
|
||||
key=f"{task_id}:{checkpoint_id}",
|
||||
value=decision,
|
||||
ttl=3600,
|
||||
)
|
||||
logger.info(
|
||||
f"收到用户决策: task={task_id}, checkpoint={checkpoint_id}, action={decision.get('action')}"
|
||||
)
|
||||
else:
|
||||
logger.warning(f"收到格式错误的 user_decision: {data}")
|
||||
else:
|
||||
logger.warning(f"收到未知类型的客户端消息: type={msg_type}")
|
||||
|
||||
|
||||
async def _listen_client(websocket: WebSocket, task_id: str) -> None:
|
||||
"""监听客户端发来的消息。
|
||||
|
||||
Args:
|
||||
websocket: WebSocket 连接。
|
||||
task_id: 任务 ID。
|
||||
"""
|
||||
try:
|
||||
while not _is_websocket_closed(websocket):
|
||||
try:
|
||||
raw = await websocket.receive_text()
|
||||
data = json.loads(raw)
|
||||
await _handle_client_message(task_id, data)
|
||||
except WebSocketDisconnect:
|
||||
logger.info(f"客户端断开连接: task={task_id}")
|
||||
break
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"客户端消息 JSON 解析失败: {e}")
|
||||
except Exception as e:
|
||||
if _is_websocket_closed(websocket):
|
||||
break
|
||||
logger.error(f"处理客户端消息时出错: {e}")
|
||||
except Exception as e:
|
||||
if not _is_websocket_closed(websocket):
|
||||
logger.error(f"客户端监听异常: {e}")
|
||||
|
||||
|
||||
async def _forward_server_messages(websocket: WebSocket, pubsub, task_id: str) -> None:
|
||||
"""转发 Redis PubSub 消息到客户端。
|
||||
|
||||
Args:
|
||||
websocket: WebSocket 连接。
|
||||
pubsub: Redis PubSub 实例。
|
||||
task_id: 任务 ID。
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
if _is_websocket_closed(websocket):
|
||||
logger.info(f"WebSocket 已关闭,停止转发 task_id: {task_id}")
|
||||
break
|
||||
try:
|
||||
msg = await pubsub.get_message(ignore_subscribe_messages=True)
|
||||
if msg:
|
||||
try:
|
||||
msg_dict = json.loads(msg["data"])
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing websocket payload: {e}")
|
||||
if _is_websocket_closed(websocket):
|
||||
break
|
||||
try:
|
||||
await ws_manager.send_personal_message_json(
|
||||
SystemMessage(
|
||||
content="实时消息解析失败,已忽略异常数据。",
|
||||
type="error",
|
||||
).model_dump(),
|
||||
websocket,
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
logger.info(
|
||||
"WebSocket disconnected while sending parse error notice"
|
||||
)
|
||||
break
|
||||
except RuntimeError as send_error:
|
||||
if _is_closed_send_error(send_error):
|
||||
logger.info("WebSocket 已关闭,跳过解析失败提示发送")
|
||||
break
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
await ws_manager.send_personal_message_json(
|
||||
msg_dict, websocket
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
logger.info("WebSocket disconnected while sending message")
|
||||
break
|
||||
except RuntimeError as send_error:
|
||||
if _is_closed_send_error(send_error):
|
||||
logger.info(
|
||||
f"WebSocket 已关闭,停止发送后续消息 task_id: {task_id}"
|
||||
)
|
||||
break
|
||||
raise
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info("WebSocket disconnected")
|
||||
break
|
||||
except Exception as e:
|
||||
if _is_closed_send_error(e) or _is_websocket_closed(websocket):
|
||||
logger.info(
|
||||
f"WebSocket 发送通道已关闭,结束循环 task_id: {task_id}"
|
||||
)
|
||||
break
|
||||
logger.error(f"Error in websocket loop: {e}")
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket error: {e}")
|
||||
|
||||
|
||||
@router.websocket("/task/{task_id}")
|
||||
async def websocket_endpoint(websocket: WebSocket, task_id: str):
|
||||
try:
|
||||
@@ -181,6 +50,7 @@ async def websocket_endpoint(websocket: WebSocket, task_id: str):
|
||||
|
||||
# 建立 WebSocket 连接
|
||||
await ws_manager.connect(websocket)
|
||||
# websocket.timeout 在 Starlette WebSocket 中不可用,已移除
|
||||
logger.debug(f"WebSocket connection status: {websocket.client}")
|
||||
|
||||
# 订阅 Redis 频道
|
||||
@@ -188,24 +58,60 @@ async def websocket_endpoint(websocket: WebSocket, task_id: str):
|
||||
logger.debug(f"Subscribed to Redis channel: task:{safe_task_id}:messages")
|
||||
|
||||
try:
|
||||
# 双向并发:服务端消息转发 + 客户端消息监听
|
||||
forward_task = asyncio.create_task(
|
||||
_forward_server_messages(websocket, pubsub, safe_task_id)
|
||||
)
|
||||
listen_task = asyncio.create_task(_listen_client(websocket, safe_task_id))
|
||||
|
||||
# 等待任一任务结束(通常是客户端断开)
|
||||
done, pending = await asyncio.wait(
|
||||
[forward_task, listen_task],
|
||||
return_when=asyncio.FIRST_COMPLETED,
|
||||
)
|
||||
# 取消未完成的任务
|
||||
for task in pending:
|
||||
task.cancel()
|
||||
while True:
|
||||
if _is_websocket_closed(websocket):
|
||||
logger.info(f"WebSocket 已关闭,停止转发 task_id: {safe_task_id}")
|
||||
break
|
||||
try:
|
||||
await task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
msg = await pubsub.get_message(ignore_subscribe_messages=True)
|
||||
if msg:
|
||||
try:
|
||||
msg_dict = json.loads(msg["data"])
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing websocket payload: {e}")
|
||||
if _is_websocket_closed(websocket):
|
||||
break
|
||||
try:
|
||||
await ws_manager.send_personal_message_json(
|
||||
SystemMessage(
|
||||
content="实时消息解析失败,已忽略异常数据。",
|
||||
type="error",
|
||||
).model_dump(),
|
||||
websocket,
|
||||
)
|
||||
except WebSocketDisconnect:
|
||||
logger.info("WebSocket disconnected while sending parse error notice")
|
||||
break
|
||||
except RuntimeError as send_error:
|
||||
if _is_closed_send_error(send_error):
|
||||
logger.info("WebSocket 已关闭,跳过解析失败提示发送")
|
||||
break
|
||||
raise
|
||||
else:
|
||||
try:
|
||||
await ws_manager.send_personal_message_json(msg_dict, websocket)
|
||||
except WebSocketDisconnect:
|
||||
logger.info("WebSocket disconnected while sending message")
|
||||
break
|
||||
except RuntimeError as send_error:
|
||||
if _is_closed_send_error(send_error):
|
||||
logger.info(
|
||||
f"WebSocket 已关闭,停止发送后续消息 task_id: {safe_task_id}"
|
||||
)
|
||||
break
|
||||
raise
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except WebSocketDisconnect:
|
||||
logger.info("WebSocket disconnected")
|
||||
break
|
||||
except Exception as e:
|
||||
if _is_closed_send_error(e) or _is_websocket_closed(websocket):
|
||||
logger.info(f"WebSocket 发送通道已关闭,结束循环 task_id: {safe_task_id}")
|
||||
break
|
||||
logger.error(f"Error in websocket loop: {e}")
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket error: {e}")
|
||||
|
||||
@@ -26,12 +26,3 @@ class WriterResponse(BaseModel):
|
||||
"""写作手的响应数据结构。"""
|
||||
response_content: Any
|
||||
footnotes: list[tuple[str, str]] | None = None
|
||||
|
||||
|
||||
class EvaluationResult(BaseModel):
|
||||
"""评估器的评估结果。"""
|
||||
passed: bool = True
|
||||
score: float = 1.0
|
||||
feedback: str = ""
|
||||
should_handoff: bool = False
|
||||
reason: str = ""
|
||||
|
||||
@@ -21,7 +21,6 @@ class AgentType(str, Enum):
|
||||
MODELER = "ModelerAgent"
|
||||
CODER = "CoderAgent"
|
||||
WRITER = "WriterAgent"
|
||||
EVALUATOR = "EvaluatorAgent"
|
||||
SYSTEM = "SystemAgent"
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
"""证据数据模型定义,为 Web Search 和 RAG 提供统一的数据抽象。"""
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from uuid import uuid4
|
||||
from datetime import datetime
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class Evidence(BaseModel):
|
||||
"""证据基类,所有搜索/检索产出的统一抽象。"""
|
||||
|
||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
content: str
|
||||
evidence_type: Literal["data", "knowledge", "code", "paper"]
|
||||
source_url: str | None = None
|
||||
source_title: str | None = None
|
||||
source_level: Literal["official", "academic", "media", "unknown"] = "unknown"
|
||||
collected_at: datetime = Field(default_factory=datetime.now)
|
||||
confidence: float = Field(default=0.5, ge=0, le=1)
|
||||
metadata: dict = Field(default_factory=dict)
|
||||
|
||||
|
||||
class DataEvidence(Evidence):
|
||||
"""Web Search 产出的结构化数据证据。"""
|
||||
|
||||
evidence_type: Literal["data", "knowledge", "code", "paper"] = "data"
|
||||
unit: str | None = None
|
||||
time_range: str | None = None
|
||||
region: str | None = None
|
||||
original_excerpt: str | None = None
|
||||
data_format: Literal["table", "timeseries", "categorical"] | None = None
|
||||
|
||||
|
||||
class KnowledgeEvidence(Evidence):
|
||||
"""RAG 检索产出的知识证据。"""
|
||||
|
||||
evidence_type: Literal["data", "knowledge", "code", "paper"] = "knowledge"
|
||||
method_name: str | None = None
|
||||
source_type: Literal["paper", "textbook", "code", "problem"] = "textbook"
|
||||
source_file: str | None = None
|
||||
@@ -8,7 +8,6 @@ from uuid import uuid4
|
||||
|
||||
class Message(BaseModel):
|
||||
"""消息基类。"""
|
||||
|
||||
id: str = Field(default_factory=lambda: str(uuid4()))
|
||||
msg_type: str # system | agent | user | tool | approval
|
||||
content: str | None = None
|
||||
@@ -45,7 +44,6 @@ class CoordinatorMessage(AgentMessage):
|
||||
|
||||
class CodeExecution(BaseModel):
|
||||
"""代码执行结果基类。"""
|
||||
|
||||
res_type: Literal["stdout", "stderr", "result", "error"]
|
||||
msg: str | None = None
|
||||
|
||||
@@ -126,7 +124,6 @@ class ApprovalMessage(Message):
|
||||
)
|
||||
timeout: int = 300
|
||||
|
||||
|
||||
# 所有可能的消息类型
|
||||
MessageType = Union[
|
||||
SystemMessage,
|
||||
@@ -139,5 +136,4 @@ MessageType = Union[
|
||||
WriterMessage,
|
||||
ModelerMessage,
|
||||
CoordinatorMessage,
|
||||
ApprovalMessage,
|
||||
]
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
"""Checkpoint 管理模块,基于 Redis 持久化的 HIL 审批流程。"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.schemas.response import ApprovalMessage
|
||||
from app.services.redis_manager import redis_manager
|
||||
from app.services.state_store import state_store
|
||||
from app.utils.log_util import logger
|
||||
|
||||
|
||||
class CheckpointManager:
|
||||
"""基于 Redis 的检查点管理器,实现 HIL 审批等待和决策提交。"""
|
||||
|
||||
def _decision_key(self, task_id: str, checkpoint_id: str) -> str:
|
||||
"""构建决策存储的键名。"""
|
||||
return f"{task_id}:{checkpoint_id}"
|
||||
|
||||
async def wait_for_decision(
|
||||
self,
|
||||
task_id: str,
|
||||
checkpoint_id: str,
|
||||
prompt: dict,
|
||||
timeout: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""发送审批请求并等待用户决策。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
checkpoint_id: 检查点 ID。
|
||||
prompt: 审批提示内容(包含当前 Agent 输出等上下文)。
|
||||
timeout: 超时时间(秒),默认使用配置值。
|
||||
|
||||
Returns:
|
||||
用户决策字典,格式: {"action": "confirm"|"edit"|..., "content": ...}
|
||||
超时时返回 {"action": "confirm"}(自动继续)。
|
||||
"""
|
||||
timeout = timeout or settings.HIL_TIMEOUT
|
||||
|
||||
# 发送审批消息到前端
|
||||
approval_msg = ApprovalMessage(
|
||||
checkpoint_id=checkpoint_id,
|
||||
prompt=prompt,
|
||||
timeout=timeout,
|
||||
)
|
||||
await redis_manager.publish_message(task_id, approval_msg)
|
||||
logger.info(
|
||||
f"CheckpointManager: 发送审批请求 checkpoint={checkpoint_id}, timeout={timeout}s"
|
||||
)
|
||||
|
||||
# 轮询 Redis 等待决策
|
||||
decision = await state_store.wait_for_update(
|
||||
namespace="checkpoint",
|
||||
key=self._decision_key(task_id, checkpoint_id),
|
||||
timeout=timeout,
|
||||
)
|
||||
|
||||
if decision is None:
|
||||
# 超时,自动继续
|
||||
logger.warning(
|
||||
f"CheckpointManager: checkpoint={checkpoint_id} 超时,自动 confirm"
|
||||
)
|
||||
return {"action": "confirm"}
|
||||
|
||||
logger.info(
|
||||
f"CheckpointManager: 收到决策 checkpoint={checkpoint_id}, action={decision.get('action')}"
|
||||
)
|
||||
return decision
|
||||
|
||||
async def submit_decision(
|
||||
self,
|
||||
task_id: str,
|
||||
checkpoint_id: str,
|
||||
decision: dict[str, Any],
|
||||
) -> None:
|
||||
"""提交用户决策到 Redis,唤醒等待的 workflow。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
checkpoint_id: 检查点 ID。
|
||||
decision: 用户决策字典。
|
||||
"""
|
||||
await state_store.set(
|
||||
namespace="checkpoint",
|
||||
key=self._decision_key(task_id, checkpoint_id),
|
||||
value=decision,
|
||||
ttl=3600, # 1 小时过期
|
||||
)
|
||||
logger.info(
|
||||
f"CheckpointManager: 提交决策 checkpoint={checkpoint_id}, action={decision.get('action')}"
|
||||
)
|
||||
|
||||
|
||||
# 全局单例
|
||||
checkpoint_manager = CheckpointManager()
|
||||
@@ -1,110 +0,0 @@
|
||||
"""基于 Redis 的持久化状态存储模块。"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from app.services.redis_manager import redis_manager
|
||||
from app.utils.log_util import logger
|
||||
|
||||
|
||||
class StateStore:
|
||||
"""Redis 持久化状态存储,支持 HIL checkpoint 等待和搜索缓存。"""
|
||||
|
||||
def _make_key(self, namespace: str, key: str) -> str:
|
||||
"""构建 Redis 键名。"""
|
||||
return f"state:{namespace}:{key}"
|
||||
|
||||
async def set(
|
||||
self, namespace: str, key: str, value: Any, ttl: int | None = None
|
||||
) -> None:
|
||||
"""存储键值对。
|
||||
|
||||
Args:
|
||||
namespace: 命名空间(如 "search", "checkpoint")。
|
||||
key: 键名。
|
||||
value: 值(会被 JSON 序列化)。
|
||||
ttl: 过期时间(秒),None 表示不过期。
|
||||
"""
|
||||
client = await redis_manager.get_client()
|
||||
redis_key = self._make_key(namespace, key)
|
||||
serialized = json.dumps(value, ensure_ascii=False)
|
||||
await client.set(redis_key, serialized)
|
||||
if ttl is not None:
|
||||
await client.expire(redis_key, ttl)
|
||||
logger.debug(f"StateStore: set {redis_key} (ttl={ttl})")
|
||||
|
||||
async def get(self, namespace: str, key: str) -> Any | None:
|
||||
"""读取键值。
|
||||
|
||||
Args:
|
||||
namespace: 命名空间。
|
||||
key: 键名。
|
||||
|
||||
Returns:
|
||||
存储的值,不存在时返回 None。
|
||||
"""
|
||||
client = await redis_manager.get_client()
|
||||
redis_key = self._make_key(namespace, key)
|
||||
raw = await client.get(redis_key)
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return raw
|
||||
|
||||
async def delete(self, namespace: str, key: str) -> None:
|
||||
"""删除键值对。
|
||||
|
||||
Args:
|
||||
namespace: 命名空间。
|
||||
key: 键名。
|
||||
"""
|
||||
client = await redis_manager.get_client()
|
||||
redis_key = self._make_key(namespace, key)
|
||||
await client.delete(redis_key)
|
||||
logger.debug(f"StateStore: delete {redis_key}")
|
||||
|
||||
async def wait_for_update(
|
||||
self, namespace: str, key: str, timeout: float = 300
|
||||
) -> Any | None:
|
||||
"""等待状态更新(轮询 Redis,用于 HIL checkpoint)。
|
||||
|
||||
Args:
|
||||
namespace: 命名空间。
|
||||
key: 键名。
|
||||
timeout: 超时时间(秒)。
|
||||
|
||||
Returns:
|
||||
更新后的值,超时返回 None。
|
||||
"""
|
||||
redis_key = self._make_key(namespace, key)
|
||||
client = await redis_manager.get_client()
|
||||
poll_interval = 1.0
|
||||
elapsed = 0.0
|
||||
|
||||
# 先检查是否已有值(避免已写入时还等待)
|
||||
raw = await client.get(redis_key)
|
||||
if raw is not None:
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return raw
|
||||
|
||||
while elapsed < timeout:
|
||||
await asyncio.sleep(poll_interval)
|
||||
elapsed += poll_interval
|
||||
raw = await client.get(redis_key)
|
||||
if raw is not None:
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return raw
|
||||
|
||||
logger.warning(f"StateStore: wait_for_update {redis_key} 超时 ({timeout}s)")
|
||||
return None
|
||||
|
||||
|
||||
# 全局单例
|
||||
state_store = StateStore()
|
||||
@@ -54,7 +54,7 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
raise
|
||||
|
||||
async def _upload_all_files(self):
|
||||
"""上传工作目录中的所有文件到沙箱"""
|
||||
"""上传工作目录中的数据文件和字体文件到沙箱"""
|
||||
try:
|
||||
logger.info(f"开始上传文件,工作目录: {self.work_dir}")
|
||||
if not os.path.exists(self.work_dir):
|
||||
@@ -62,7 +62,8 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
raise FileNotFoundError(f"工作目录不存在: {self.work_dir}")
|
||||
|
||||
files = [
|
||||
f for f in os.listdir(self.work_dir) if f.endswith((".csv", ".xlsx"))
|
||||
f for f in os.listdir(self.work_dir)
|
||||
if f.endswith((".csv", ".xlsx", ".ttf", ".otf", ".ttc"))
|
||||
]
|
||||
logger.info(f"工作目录中的文件列表: {files}")
|
||||
|
||||
@@ -72,8 +73,6 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
content = f.read()
|
||||
# 使用官方推荐的 files.write 方法
|
||||
assert self.sbx is not None
|
||||
await self.sbx.files.write(f"/home/user/{file}", content)
|
||||
logger.info(f"成功上传文件到沙箱: {file}")
|
||||
except Exception as e:
|
||||
@@ -86,10 +85,18 @@ class E2BCodeInterpreter(BaseCodeInterpreter):
|
||||
|
||||
async def _pre_execute_code(self):
|
||||
init_code = (
|
||||
# 从 /home/user 加载上传的字体文件(跨平台兼容,无需 apt-get)
|
||||
"import os\n"
|
||||
"import matplotlib\n"
|
||||
"import matplotlib.pyplot as plt\n"
|
||||
# "plt.rcParams['font.sans-serif'] = ['DejaVu Sans', 'Arial Unicode MS']\n"
|
||||
# "plt.rcParams['axes.unicode_minus'] = False\n"
|
||||
# "plt.rcParams['font.family'] = 'sans-serif'\n"
|
||||
"from matplotlib import font_manager\n"
|
||||
"_font_dir = '/home/user'\n"
|
||||
"for _f in os.listdir(_font_dir):\n"
|
||||
" if _f.lower().endswith(('.ttf', '.otf', '.ttc')):\n"
|
||||
" font_manager.fontManager.addfont(os.path.join(_font_dir, _f))\n"
|
||||
"plt.rcParams['font.sans-serif'] = ['SimHei', 'Noto Sans CJK SC', 'WenQuanYi Micro Hei', 'Noto Sans SC', 'Microsoft YaHei', 'DejaVu Sans', 'sans-serif']\n"
|
||||
"plt.rcParams['axes.unicode_minus'] = False\n"
|
||||
"plt.rcParams['font.family'] = 'sans-serif'\n"
|
||||
)
|
||||
await self.execute_code(init_code)
|
||||
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
"""RAG 知识检索模块,从 ChromaDB 检索专业知识并产出 KnowledgeEvidence。"""
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.schemas.evidence import KnowledgeEvidence
|
||||
from app.utils.log_util import logger
|
||||
|
||||
|
||||
class KnowledgeRetriever:
|
||||
"""知识检索器,支持 Dense (ChromaDB) + Metadata Filter + Rerank。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._collection = None
|
||||
self._reranker = None
|
||||
self._initialized = False
|
||||
|
||||
async def _ensure_initialized(self) -> None:
|
||||
"""延迟初始化 ChromaDB 和 Reranker。"""
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
try:
|
||||
import chromadb # type: ignore[import-unresolved]
|
||||
|
||||
client = chromadb.PersistentClient(path=settings.RAG_DB_PATH)
|
||||
self._collection = client.get_or_create_collection(
|
||||
name="knowledge",
|
||||
metadata={"hnsw:space": "cosine"},
|
||||
)
|
||||
logger.info(f"ChromaDB 初始化完成,路径: {settings.RAG_DB_PATH}")
|
||||
except Exception as e:
|
||||
logger.error(f"ChromaDB 初始化失败: {e}")
|
||||
self._collection = None
|
||||
|
||||
try:
|
||||
from sentence_transformers import CrossEncoder # type: ignore[import-unresolved]
|
||||
|
||||
self._reranker = CrossEncoder(settings.RAG_RERANKER_MODEL)
|
||||
logger.info(f"Reranker 加载完成: {settings.RAG_RERANKER_MODEL}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Reranker 加载失败(将跳过 rerank): {e}")
|
||||
self._reranker = None
|
||||
|
||||
self._initialized = True
|
||||
|
||||
async def retrieve(
|
||||
self,
|
||||
query: str,
|
||||
top_k: int | None = None,
|
||||
source_type: str | None = None,
|
||||
method_name: str | None = None,
|
||||
) -> list[KnowledgeEvidence]:
|
||||
"""检索与查询相关的知识证据。
|
||||
|
||||
Args:
|
||||
query: 检索查询。
|
||||
top_k: 返回结果数量,默认使用配置值。
|
||||
source_type: 过滤来源类型("paper"|"textbook"|"code"|"problem")。
|
||||
method_name: 过滤方法名(如 "TOPSIS")。
|
||||
|
||||
Returns:
|
||||
KnowledgeEvidence 列表。
|
||||
"""
|
||||
await self._ensure_initialized()
|
||||
|
||||
if self._collection is None:
|
||||
logger.warning("ChromaDB 未初始化,跳过知识检索")
|
||||
return []
|
||||
|
||||
top_k = top_k or settings.RAG_TOP_K
|
||||
|
||||
# 构建 metadata 过滤条件
|
||||
where = self._build_where_filter(source_type, method_name)
|
||||
|
||||
try:
|
||||
# Dense search
|
||||
results = self._collection.query(
|
||||
query_texts=[query],
|
||||
n_results=top_k * 2, # 多取一些用于 rerank
|
||||
where=where if where else None,
|
||||
include=["documents", "metadatas", "distances"],
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"ChromaDB 查询失败: {e}")
|
||||
return []
|
||||
|
||||
if not results or not results["documents"] or not results["documents"][0]:
|
||||
return []
|
||||
|
||||
documents = results["documents"][0]
|
||||
metadatas: list[dict[str, object]] = (
|
||||
[dict(m) for m in results["metadatas"][0]]
|
||||
if results["metadatas"]
|
||||
else [{}] * len(documents)
|
||||
)
|
||||
distances = (
|
||||
results["distances"][0] if results["distances"] else [0.0] * len(documents)
|
||||
)
|
||||
|
||||
# Rerank
|
||||
if self._reranker and len(documents) > top_k:
|
||||
documents, metadatas, distances = self._rerank(
|
||||
query, documents, metadatas, distances, top_k
|
||||
)
|
||||
|
||||
# 构建 KnowledgeEvidence
|
||||
evidence_list = []
|
||||
for doc, meta, dist in zip(
|
||||
documents[:top_k], metadatas[:top_k], distances[:top_k]
|
||||
):
|
||||
confidence = max(0.0, 1.0 - dist) # cosine distance → confidence
|
||||
source_type_raw = meta.get("source_type", "textbook")
|
||||
source_type = source_type_raw if source_type_raw in ("paper", "textbook", "code", "problem") else "textbook"
|
||||
evidence = KnowledgeEvidence(
|
||||
content=doc,
|
||||
source_type=source_type, # type: ignore[arg-type]
|
||||
method_name=str(meta["method_name"]) if meta.get("method_name") else None,
|
||||
source_file=str(meta["source_file"]) if meta.get("source_file") else None,
|
||||
source_url=str(meta["source_url"]) if meta.get("source_url") else None,
|
||||
source_title=str(meta["source_title"]) if meta.get("source_title") else None,
|
||||
confidence=min(1.0, confidence),
|
||||
metadata=dict(meta),
|
||||
)
|
||||
evidence_list.append(evidence)
|
||||
|
||||
logger.info(f"知识检索完成: query={query}, 找到 {len(evidence_list)} 条")
|
||||
return evidence_list
|
||||
|
||||
def _build_where_filter(
|
||||
self, source_type: str | None, method_name: str | None
|
||||
) -> dict | None:
|
||||
"""构建 ChromaDB metadata 过滤条件。"""
|
||||
conditions = []
|
||||
if source_type:
|
||||
# 支持逗号分隔的多值过滤
|
||||
if "," in source_type:
|
||||
types = [t.strip() for t in source_type.split(",")]
|
||||
conditions.append({"source_type": {"$in": types}})
|
||||
else:
|
||||
conditions.append({"source_type": source_type})
|
||||
if method_name:
|
||||
conditions.append({"method_name": method_name})
|
||||
|
||||
if len(conditions) == 0:
|
||||
return None
|
||||
if len(conditions) == 1:
|
||||
return conditions[0]
|
||||
return {"$and": conditions}
|
||||
|
||||
def _rerank(
|
||||
self,
|
||||
query: str,
|
||||
documents: list[str],
|
||||
metadatas: list[dict[str, object]],
|
||||
distances: list[float],
|
||||
top_k: int,
|
||||
) -> tuple[list[str], list[dict[str, object]], list[float]]:
|
||||
"""使用 CrossEncoder 对检索结果重排序。"""
|
||||
pairs = [(query, doc) for doc in documents]
|
||||
scores = self._reranker.predict(pairs) # type: ignore[union-attr]
|
||||
|
||||
# 按 rerank 分数降序排列
|
||||
scored = list(zip(documents, metadatas, distances, scores))
|
||||
scored.sort(key=lambda x: x[3], reverse=True)
|
||||
|
||||
reranked_docs = [x[0] for x in scored[:top_k]]
|
||||
reranked_metas = [x[1] for x in scored[:top_k]]
|
||||
# 将 rerank 分数转换为 distance(越小越好 → 越大越好)
|
||||
reranked_dists = [1.0 - float(x[3]) for x in scored[:top_k]]
|
||||
|
||||
return reranked_docs, reranked_metas, reranked_dists
|
||||
|
||||
|
||||
# 全局单例
|
||||
knowledge_retriever = KnowledgeRetriever()
|
||||
@@ -30,29 +30,33 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
# 本地内核一般不需异步上传文件,直接切换目录即可
|
||||
# 初始化 Jupyter 内核管理器和客户端
|
||||
logger.info("初始化本地内核")
|
||||
# 设置 UTF-8 编码环境,避免 Windows 中文环境下 GBK 编码导致的乱码问题
|
||||
kernel_env = os.environ.copy()
|
||||
kernel_env["PYTHONIOENCODING"] = "utf-8"
|
||||
kernel_env["PYTHONUTF8"] = "1"
|
||||
self.km, self.kc = jupyter_client.manager.start_new_kernel(
|
||||
kernel_name="python3"
|
||||
kernel_name="python3", env=kernel_env
|
||||
)
|
||||
self._pre_execute_code()
|
||||
|
||||
def _pre_execute_code(self): # type: ignore[reportIncompatibleMethodOverride]
|
||||
def _pre_execute_code(self):
|
||||
init_code = (
|
||||
f"import os\n"
|
||||
f"work_dir = r'{self.work_dir}'\n"
|
||||
f"os.makedirs(work_dir, exist_ok=True)\n"
|
||||
f"os.chdir(work_dir)\n"
|
||||
f"print('当前工作目录:', os.getcwd())\n"
|
||||
# f"import matplotlib.pyplot as plt\n"
|
||||
# f"import matplotlib as mpl\n"
|
||||
# # 更完整的中文字体配置
|
||||
# f"plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'SimHei', 'Microsoft YaHei', 'WenQuanYi Micro Hei', 'PingFang SC', 'Hiragino Sans GB', 'Heiti SC', 'DejaVu Sans', 'sans-serif']\n"
|
||||
# f"plt.rcParams['axes.unicode_minus'] = False\n"
|
||||
# f"plt.rcParams['font.family'] = 'sans-serif'\n"
|
||||
# f"mpl.rcParams['font.size'] = 12\n"
|
||||
# f"mpl.rcParams['axes.labelsize'] = 12\n"
|
||||
# f"mpl.rcParams['xtick.labelsize'] = 10\n"
|
||||
# f"mpl.rcParams['ytick.labelsize'] = 10\n"
|
||||
# # 设置DPI以获得更清晰的显示
|
||||
# 从工作目录加载字体,确保图表中文正常显示(跨平台兼容)
|
||||
f"import matplotlib\n"
|
||||
f"import matplotlib.pyplot as plt\n"
|
||||
f"from matplotlib import font_manager\n"
|
||||
f"_font_dir = work_dir\n"
|
||||
f"for _f in os.listdir(_font_dir):\n"
|
||||
f" if _f.lower().endswith(('.ttf', '.otf', '.ttc')):\n"
|
||||
f" font_manager.fontManager.addfont(os.path.join(_font_dir, _f))\n"
|
||||
f"plt.rcParams['font.sans-serif'] = ['SimHei', 'Noto Sans CJK SC', 'WenQuanYi Micro Hei', 'Noto Sans SC', 'Microsoft YaHei', 'DejaVu Sans', 'sans-serif']\n"
|
||||
f"plt.rcParams['axes.unicode_minus'] = False\n"
|
||||
f"plt.rcParams['font.family'] = 'sans-serif'\n"
|
||||
)
|
||||
self.execute_code_(init_code)
|
||||
|
||||
@@ -229,11 +233,16 @@ class LocalCodeInterpreter(BaseCodeInterpreter):
|
||||
"""Restart the Jupyter kernel and recreate the work directory."""
|
||||
assert self.kc is not None
|
||||
self.kc.shutdown()
|
||||
# 设置 UTF-8 编码环境,避免 Windows 中文环境下 GBK 编码导致的乱码问题
|
||||
kernel_env = os.environ.copy()
|
||||
kernel_env["PYTHONIOENCODING"] = "utf-8"
|
||||
kernel_env["PYTHONUTF8"] = "1"
|
||||
self.km, self.kc = jupyter_client.manager.start_new_kernel(
|
||||
kernel_name="python3"
|
||||
kernel_name="python3", env=kernel_env
|
||||
)
|
||||
self.interrupt_signal = False
|
||||
self._create_work_dir()
|
||||
self._pre_execute_code()
|
||||
|
||||
def _create_work_dir(self):
|
||||
"""Ensure the working directory exists after a restart."""
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
"""工具注册与分发模块,替代各 Agent 中的 if/elif 硬编码分发。"""
|
||||
|
||||
from typing import Callable, Awaitable
|
||||
from app.utils.log_util import logger
|
||||
|
||||
|
||||
class ToolRegistry:
|
||||
"""统一工具注册表,管理 tool schema 和 handler 分发。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._handlers: dict[str, Callable[..., Awaitable[str]]] = {}
|
||||
self._schemas: dict[str, dict] = {}
|
||||
|
||||
def register(
|
||||
self,
|
||||
name: str,
|
||||
handler: Callable[..., Awaitable[str]],
|
||||
schema: dict,
|
||||
) -> None:
|
||||
"""注册工具。
|
||||
|
||||
Args:
|
||||
name: 工具名称,需与 schema 中的 function.name 一致。
|
||||
handler: 异步处理函数,签名 (arguments: dict, task_id: str) -> str。
|
||||
schema: OpenAI function-calling 格式的 tool schema。
|
||||
"""
|
||||
self._handlers[name] = handler
|
||||
self._schemas[name] = schema
|
||||
logger.info(f"ToolRegistry: 注册工具 {name}")
|
||||
|
||||
def get_schemas(self, names: list[str] | None = None) -> list[dict]:
|
||||
"""获取指定工具的 OpenAI function-calling schema 列表。
|
||||
|
||||
Args:
|
||||
names: 工具名称列表,None 表示返回所有已注册工具。
|
||||
|
||||
Returns:
|
||||
schema 列表。
|
||||
"""
|
||||
if names is None:
|
||||
return list(self._schemas.values())
|
||||
return [self._schemas[n] for n in names if n in self._schemas]
|
||||
|
||||
async def dispatch(self, name: str, arguments: dict, task_id: str) -> str:
|
||||
"""分发工具调用到对应 handler。
|
||||
|
||||
Args:
|
||||
name: 工具名称。
|
||||
arguments: 工具参数。
|
||||
task_id: 当前任务 ID。
|
||||
|
||||
Returns:
|
||||
工具执行结果字符串。
|
||||
|
||||
Raises:
|
||||
ValueError: 工具未注册时抛出。
|
||||
"""
|
||||
handler = self._handlers.get(name)
|
||||
if handler is None:
|
||||
raise ValueError(f"ToolRegistry: 未注册的工具 {name}")
|
||||
logger.info(f"ToolRegistry: 分发工具调用 {name}")
|
||||
return await handler(arguments, task_id)
|
||||
|
||||
|
||||
# 全局单例
|
||||
tool_registry = ToolRegistry()
|
||||
@@ -1,187 +0,0 @@
|
||||
"""Web Search 工具模块,Agent 主动生成搜索查询并提取结构化数据证据。"""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
import httpx
|
||||
|
||||
from app.config.setting import settings
|
||||
from app.core.llm.llm import LLM, simple_chat
|
||||
from app.schemas.evidence import DataEvidence
|
||||
from app.schemas.response import SystemMessage
|
||||
from app.services.redis_manager import redis_manager
|
||||
from app.services.state_store import state_store
|
||||
from app.utils.log_util import logger
|
||||
|
||||
# LLM 提取结构化数据的 prompt
|
||||
_EXTRACT_PROMPT = """你是一个数据提取专家。从以下网页搜索结果中提取结构化数据。
|
||||
|
||||
搜索查询:{query}
|
||||
|
||||
搜索结果:
|
||||
{results}
|
||||
|
||||
请提取所有有价值的结构化数据,以 JSON 数组格式返回,每个元素包含:
|
||||
- content: 数据描述(简洁明了)
|
||||
- unit: 数据单位(如"亿元"、"万人"、"%"),无明确单位填 null
|
||||
- time_range: 数据时间范围(如"2020-2024"),无明确时间填 null
|
||||
- region: 数据地域范围(如"中国"、"全球"),无明确地域填 null
|
||||
- original_excerpt: 原文摘录(包含具体数字的原文片段)
|
||||
- source_url: 数据来源 URL
|
||||
- source_title: 来源标题
|
||||
- source_level: 来源级别,"official"(政府/官方)| "academic"(学术)| "media"(媒体)| "unknown"
|
||||
- confidence: 数据可信度 0-1(官方>学术>媒体>未知)
|
||||
- data_format: 数据格式 "table"|"timeseries"|"categorical"
|
||||
|
||||
只返回 JSON 数组,不要其他文字。如果没有有价值的数据,返回空数组 []。"""
|
||||
|
||||
|
||||
class WebSearcher:
|
||||
"""Web 搜索工具,调用 Tavily API 获取网页数据并用 LLM 提取结构化证据。"""
|
||||
|
||||
def __init__(self, llm: LLM) -> None:
|
||||
"""初始化 WebSearcher。
|
||||
|
||||
Args:
|
||||
llm: 用于提取结构化数据的 LLM 实例。
|
||||
"""
|
||||
self.llm = llm
|
||||
|
||||
def _cache_key(self, query: str) -> str:
|
||||
"""生成缓存键。"""
|
||||
return hashlib.md5(query.encode()).hexdigest()
|
||||
|
||||
async def search(
|
||||
self, query: str, data_type: str = "general", max_results: int = 5
|
||||
) -> list[DataEvidence]:
|
||||
"""执行 Web 搜索并提取结构化数据证据。
|
||||
|
||||
Args:
|
||||
query: 搜索查询。
|
||||
data_type: 数据类型提示(如 "statistical", "timeseries")。
|
||||
max_results: 最大结果数。
|
||||
|
||||
Returns:
|
||||
DataEvidence 列表。
|
||||
"""
|
||||
if not settings.TAVILY_API_KEY:
|
||||
logger.warning("TAVILY_API_KEY 未配置,跳过 Web 搜索")
|
||||
return []
|
||||
|
||||
# 查缓存
|
||||
cache_key = self._cache_key(f"{query}_{data_type}_{max_results}")
|
||||
cached = await state_store.get("search", cache_key)
|
||||
if cached is not None:
|
||||
logger.info(f"WebSearcher: 命中缓存 query={query}")
|
||||
return [DataEvidence(**item) for item in cached]
|
||||
|
||||
# 发布搜索中消息
|
||||
await redis_manager.publish_message(
|
||||
self.llm.task_id,
|
||||
SystemMessage(content=f"正在搜索: {query}"),
|
||||
)
|
||||
|
||||
# 调用 Tavily API
|
||||
raw_results = await self._tavily_search(query, max_results)
|
||||
if not raw_results:
|
||||
return []
|
||||
|
||||
# 用 LLM 提取结构化数据
|
||||
evidence_list = await self._extract_evidence(query, raw_results)
|
||||
|
||||
# 缓存结果
|
||||
if evidence_list:
|
||||
await state_store.set(
|
||||
"search",
|
||||
cache_key,
|
||||
[e.model_dump() for e in evidence_list],
|
||||
ttl=settings.SEARCH_CACHE_TTL,
|
||||
)
|
||||
|
||||
# 发布搜索完成消息
|
||||
await redis_manager.publish_message(
|
||||
self.llm.task_id,
|
||||
SystemMessage(content=f"搜索完成: 找到 {len(evidence_list)} 条数据证据"),
|
||||
)
|
||||
|
||||
return evidence_list
|
||||
|
||||
async def _tavily_search(self, query: str, max_results: int) -> list[dict]:
|
||||
"""调用 Tavily Search API。
|
||||
|
||||
Args:
|
||||
query: 搜索查询。
|
||||
max_results: 最大结果数。
|
||||
|
||||
Returns:
|
||||
搜索结果列表。
|
||||
"""
|
||||
url = "https://api.tavily.com/search"
|
||||
payload = {
|
||||
"api_key": settings.TAVILY_API_KEY,
|
||||
"query": query,
|
||||
"search_depth": "advanced",
|
||||
"max_results": max_results,
|
||||
"include_answer": True,
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=30) as client:
|
||||
response = await client.post(url, json=payload)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data.get("results", [])
|
||||
except Exception as e:
|
||||
logger.error(f"Tavily API 调用失败: {e}")
|
||||
return []
|
||||
|
||||
async def _extract_evidence(
|
||||
self, query: str, raw_results: list[dict]
|
||||
) -> list[DataEvidence]:
|
||||
"""用 LLM 从搜索结果中提取结构化数据证据。
|
||||
|
||||
Args:
|
||||
query: 原始搜索查询。
|
||||
raw_results: Tavily API 返回的原始结果。
|
||||
|
||||
Returns:
|
||||
DataEvidence 列表。
|
||||
"""
|
||||
# 格式化搜索结果给 LLM
|
||||
results_text = ""
|
||||
for i, r in enumerate(raw_results, 1):
|
||||
results_text += f"\n--- 结果 {i} ---\n"
|
||||
results_text += f"标题: {r.get('title', '')}\n"
|
||||
results_text += f"URL: {r.get('url', '')}\n"
|
||||
results_text += f"内容: {r.get('content', '')[:1500]}\n"
|
||||
|
||||
prompt = _EXTRACT_PROMPT.format(query=query, results=results_text)
|
||||
|
||||
try:
|
||||
response_text = await simple_chat(
|
||||
self.llm,
|
||||
[{"role": "user", "content": prompt}],
|
||||
)
|
||||
# 清理 JSON
|
||||
response_text = response_text.strip()
|
||||
if response_text.startswith("```"):
|
||||
response_text = response_text.split("\n", 1)[-1]
|
||||
if response_text.endswith("```"):
|
||||
response_text = response_text.rsplit("```", 1)[0]
|
||||
response_text = response_text.strip()
|
||||
|
||||
items = json.loads(response_text)
|
||||
if not isinstance(items, list):
|
||||
return []
|
||||
|
||||
evidence_list = []
|
||||
for item in items:
|
||||
try:
|
||||
evidence_list.append(DataEvidence(**item))
|
||||
except Exception:
|
||||
continue
|
||||
return evidence_list
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM 数据提取失败: {e}")
|
||||
return []
|
||||
@@ -1,6 +1,7 @@
|
||||
"""通用工具函数模块,提供任务 ID 生成、文件操作和文档转换等功能。"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import datetime
|
||||
import hashlib
|
||||
import tomllib
|
||||
@@ -40,7 +41,7 @@ def ensure_safe_task_id(task_id: str) -> str:
|
||||
|
||||
|
||||
def create_work_dir(task_id: str) -> str:
|
||||
"""为指定任务创建工作目录。
|
||||
"""为指定任务创建工作目录,并复制字体文件到工作目录。
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID。
|
||||
@@ -54,6 +55,8 @@ def create_work_dir(task_id: str) -> str:
|
||||
try:
|
||||
# 创建目录,如果目录已存在也不会报错
|
||||
os.makedirs(work_dir, exist_ok=True)
|
||||
# 复制字体文件到工作目录,确保图表中文正常显示
|
||||
_copy_fonts_to_work_dir(work_dir)
|
||||
return work_dir
|
||||
except Exception as e:
|
||||
# 捕获并记录创建目录时的异常
|
||||
@@ -61,6 +64,33 @@ def create_work_dir(task_id: str) -> str:
|
||||
raise
|
||||
|
||||
|
||||
# 字体源目录(backend/fonts/)
|
||||
_FONTS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "fonts")
|
||||
|
||||
|
||||
def _copy_fonts_to_work_dir(work_dir: str) -> None:
|
||||
"""将后端字体目录中的字体文件复制到工作目录。
|
||||
|
||||
Args:
|
||||
work_dir: 目标工作目录路径。
|
||||
"""
|
||||
fonts_dir = os.path.normpath(_FONTS_DIR)
|
||||
if not os.path.isdir(fonts_dir):
|
||||
logger.warning(f"字体目录不存在: {fonts_dir}")
|
||||
return
|
||||
|
||||
for filename in os.listdir(fonts_dir):
|
||||
if not filename.lower().endswith((".ttf", ".otf", ".ttc")):
|
||||
continue
|
||||
src = os.path.join(fonts_dir, filename)
|
||||
dst = os.path.join(work_dir, filename)
|
||||
try:
|
||||
shutil.copy2(src, dst)
|
||||
logger.debug(f"复制字体: {filename} -> {work_dir}")
|
||||
except Exception as e:
|
||||
logger.warning(f"复制字体 {filename} 失败: {e}")
|
||||
|
||||
|
||||
def get_work_dir(task_id: str) -> str:
|
||||
"""获取指定任务的工作目录路径。
|
||||
|
||||
|
||||
@@ -23,11 +23,9 @@ class LoggerInitializer:
|
||||
os.mkdir(self.log_path)
|
||||
|
||||
@staticmethod
|
||||
def __filter(log: dict):
|
||||
"""
|
||||
自定义日志过滤器,添加trace_id
|
||||
"""
|
||||
return log
|
||||
def __filter(record):
|
||||
"""自定义日志过滤器,保留所有日志。"""
|
||||
return True
|
||||
|
||||
def init_log(self):
|
||||
"""
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
"""LLM 调用指标收集模块。"""
|
||||
|
||||
from litellm.integrations.custom_logger import CustomLogger # type: ignore[import-unresolved]
|
||||
from app.utils.log_util import logger
|
||||
|
||||
|
||||
class AgentMetrics(CustomLogger):
|
||||
"""LLM 调用指标收集器,记录成功的 API 调用信息。"""
|
||||
def log_agent_call(agent_name: str, success: bool = True) -> None:
|
||||
"""记录 LLM 调用日志。
|
||||
|
||||
async def async_log_success_event(self, kwargs, response_obj, start_time, end_time):
|
||||
try:
|
||||
# response_cost = kwargs.get("response_cost", 0)
|
||||
# print("streaming response_cost", response_cost)
|
||||
print("agent_name", kwargs["litellm_params"]["metadata"]["agent_name"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def async_log_failure_event(self, kwargs, response_obj, start_time, end_time):
|
||||
print("On Async Failure")
|
||||
|
||||
|
||||
# 全局指标收集器实例
|
||||
agent_metrics = AgentMetrics()
|
||||
Args:
|
||||
agent_name: Agent 名称。
|
||||
success: 调用是否成功。
|
||||
"""
|
||||
status = "成功" if success else "失败"
|
||||
logger.info(f"LLM 调用 {status}: {agent_name}")
|
||||
|
||||
Binary file not shown.
@@ -8,6 +8,7 @@ dependencies = [
|
||||
"aiofile>=3.9.0",
|
||||
"aioredis>=2.0.1",
|
||||
"ansi2html>=1.9.2",
|
||||
"anthropic>=0.40.0",
|
||||
"celery>=5.4.0",
|
||||
"chromadb>=0.4.0",
|
||||
"e2b-code-interpreter>=1.0.5",
|
||||
@@ -16,7 +17,6 @@ dependencies = [
|
||||
"icecream>=2.1.4",
|
||||
"ipykernel>=6.29.5",
|
||||
"jupyter-client>=8.6.3",
|
||||
"litellm>=1.69.0",
|
||||
"loguru>=0.7.3",
|
||||
"matplotlib>=3.10.1",
|
||||
"nbformat>=5.10.4",
|
||||
@@ -30,6 +30,7 @@ dependencies = [
|
||||
"pypandoc-binary>=1.15",
|
||||
"rank-bm25>=0.2.2",
|
||||
"redis>=5.2.1",
|
||||
"requests>=2.32.0",
|
||||
"scikit-learn>=1.6.1",
|
||||
"scipy>=1.15.2",
|
||||
"seaborn>=0.13.2",
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"venvPath": ".",
|
||||
"venv": ".venv",
|
||||
"pythonVersion": "3.12"
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
"""离线构建知识库脚本,扫描 data/knowledge/ 目录并构建 ChromaDB 索引。
|
||||
|
||||
使用方法:
|
||||
cd backend
|
||||
python scripts/build_knowledge_base.py
|
||||
|
||||
目录结构:
|
||||
data/knowledge/
|
||||
papers/ # 论文 PDF
|
||||
textbooks/ # 教材(Markdown/文本)
|
||||
code/ # 代码模板
|
||||
problems/ # 优秀论文方案
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到 sys.path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
KNOWLEDGE_DIR = Path("data/knowledge")
|
||||
CHROMADB_PATH = "data/chromadb"
|
||||
EMBEDDING_MODEL = "BAAI/bge-m3"
|
||||
CHUNK_SIZE = 500 # 字符数
|
||||
CHUNK_OVERLAP = 50
|
||||
|
||||
|
||||
def chunk_text(
|
||||
text: str, chunk_size: int = CHUNK_SIZE, overlap: int = CHUNK_OVERLAP
|
||||
) -> list[str]:
|
||||
"""将文本按固定大小分块。
|
||||
|
||||
Args:
|
||||
text: 原始文本。
|
||||
chunk_size: 每块大小(字符数)。
|
||||
overlap: 块间重叠字符数。
|
||||
|
||||
Returns:
|
||||
文本块列表。
|
||||
"""
|
||||
chunks = []
|
||||
start = 0
|
||||
while start < len(text):
|
||||
end = start + chunk_size
|
||||
chunk = text[start:end]
|
||||
if chunk.strip():
|
||||
chunks.append(chunk.strip())
|
||||
start = end - overlap
|
||||
return chunks
|
||||
|
||||
|
||||
def extract_pdf_text(pdf_path: str) -> str:
|
||||
"""从 PDF 提取文本(使用 PyMuPDF)。
|
||||
|
||||
Args:
|
||||
pdf_path: PDF 文件路径。
|
||||
|
||||
Returns:
|
||||
提取的文本内容。
|
||||
"""
|
||||
try:
|
||||
import fitz # PyMuPDF
|
||||
|
||||
doc = fitz.open(pdf_path)
|
||||
text_parts = []
|
||||
for page in doc:
|
||||
text_parts.append(page.get_text())
|
||||
doc.close()
|
||||
return "\n".join(text_parts)
|
||||
except ImportError:
|
||||
print("警告: PyMuPDF 未安装,跳过 PDF 文件。请运行: pip install pymupdf")
|
||||
return ""
|
||||
except Exception as e:
|
||||
print(f"PDF 提取失败 {pdf_path}: {e}")
|
||||
return ""
|
||||
|
||||
|
||||
def scan_knowledge_dir() -> list[dict]:
|
||||
"""扫描知识库目录,收集所有待索引的文档。
|
||||
|
||||
Returns:
|
||||
文档信息列表,每项包含 text, metadata。
|
||||
"""
|
||||
documents = []
|
||||
|
||||
if not KNOWLEDGE_DIR.exists():
|
||||
print(f"知识库目录不存在: {KNOWLEDGE_DIR}")
|
||||
print("请创建目录结构:")
|
||||
print(f" {KNOWLEDGE_DIR}/papers/ # 论文 PDF")
|
||||
print(f" {KNOWLEDGE_DIR}/textbooks/ # 教材")
|
||||
print(f" {KNOWLEDGE_DIR}/code/ # 代码模板")
|
||||
print(f" {KNOWLEDGE_DIR}/problems/ # 优秀论文方案")
|
||||
return documents
|
||||
|
||||
# 映射目录名到 source_type
|
||||
dir_type_map = {
|
||||
"papers": "paper",
|
||||
"textbooks": "textbook",
|
||||
"code": "code",
|
||||
"problems": "problem",
|
||||
}
|
||||
|
||||
for dir_name, source_type in dir_type_map.items():
|
||||
dir_path = KNOWLEDGE_DIR / dir_name
|
||||
if not dir_path.exists():
|
||||
continue
|
||||
|
||||
for file_path in dir_path.rglob("*"):
|
||||
if file_path.is_dir():
|
||||
continue
|
||||
|
||||
suffix = file_path.suffix.lower()
|
||||
text = ""
|
||||
|
||||
if suffix == ".pdf":
|
||||
text = extract_pdf_text(str(file_path))
|
||||
elif suffix in (".md", ".txt", ".rst"):
|
||||
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
||||
elif suffix in (".py", ".jl", ".r"):
|
||||
text = file_path.read_text(encoding="utf-8", errors="ignore")
|
||||
else:
|
||||
continue
|
||||
|
||||
if not text.strip():
|
||||
continue
|
||||
|
||||
# 尝试从文件名提取方法名
|
||||
method_name = file_path.stem.replace("_", " ").replace("-", " ")
|
||||
|
||||
documents.append(
|
||||
{
|
||||
"text": text,
|
||||
"metadata": {
|
||||
"source_type": source_type,
|
||||
"source_file": str(file_path),
|
||||
"source_title": file_path.stem,
|
||||
"method_name": method_name,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
print(f"扫描完成,找到 {len(documents)} 个文档")
|
||||
return documents
|
||||
|
||||
|
||||
def build_chromadb(documents: list[dict]) -> None:
|
||||
"""构建 ChromaDB 索引。
|
||||
|
||||
Args:
|
||||
documents: 文档信息列表。
|
||||
"""
|
||||
try:
|
||||
import chromadb
|
||||
except ImportError:
|
||||
print("错误: chromadb 未安装。请运行: pip install chromadb")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
from sentence_transformers import SentenceTransformer
|
||||
except ImportError:
|
||||
print(
|
||||
"错误: sentence-transformers 未安装。请运行: pip install sentence-transformers"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"加载 Embedding 模型: {EMBEDDING_MODEL}")
|
||||
model = SentenceTransformer(EMBEDDING_MODEL)
|
||||
|
||||
client = chromadb.PersistentClient(path=CHROMADB_PATH)
|
||||
# 删除旧集合重建
|
||||
try:
|
||||
client.delete_collection("knowledge")
|
||||
print("已删除旧的 knowledge 集合")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
collection = client.create_collection(
|
||||
name="knowledge",
|
||||
metadata={"hnsw:space": "cosine"},
|
||||
)
|
||||
|
||||
# 分块并索引
|
||||
all_ids = []
|
||||
all_documents = []
|
||||
all_metadatas = []
|
||||
doc_idx = 0
|
||||
|
||||
for doc_info in documents:
|
||||
chunks = chunk_text(doc_info["text"])
|
||||
for chunk in chunks:
|
||||
all_ids.append(f"doc_{doc_idx}")
|
||||
all_documents.append(chunk)
|
||||
all_metadatas.append(doc_info["metadata"])
|
||||
doc_idx += 1
|
||||
|
||||
print(f"共 {len(all_documents)} 个文本块,开始生成 embeddings...")
|
||||
|
||||
# 批量生成 embeddings
|
||||
batch_size = 64
|
||||
for i in range(0, len(all_documents), batch_size):
|
||||
batch_docs = all_documents[i : i + batch_size]
|
||||
batch_ids = all_ids[i : i + batch_size]
|
||||
batch_metas = all_metadatas[i : i + batch_size]
|
||||
|
||||
embeddings = model.encode(batch_docs, show_progress_bar=False).tolist()
|
||||
|
||||
collection.add(
|
||||
ids=batch_ids,
|
||||
documents=batch_docs,
|
||||
embeddings=embeddings,
|
||||
metadatas=batch_metas,
|
||||
)
|
||||
progress = min(i + batch_size, len(all_documents))
|
||||
print(f" 已索引: {progress}/{len(all_documents)}")
|
||||
|
||||
print(f"ChromaDB 构建完成,保存至: {CHROMADB_PATH}")
|
||||
print(f"共索引 {len(all_documents)} 个文本块,来自 {len(documents)} 个文档")
|
||||
|
||||
|
||||
def main():
|
||||
"""主入口。"""
|
||||
print("=" * 60)
|
||||
print("MathModelAgent 知识库构建工具")
|
||||
print("=" * 60)
|
||||
|
||||
documents = scan_knowledge_dir()
|
||||
if not documents:
|
||||
print("没有找到可索引的文档,退出。")
|
||||
return
|
||||
|
||||
build_chromadb(documents)
|
||||
print("\n构建完成!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+35
-363
@@ -22,64 +22,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
version = "2.6.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.11.18"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
{ name = "aiosignal" },
|
||||
{ name = "attrs" },
|
||||
{ name = "frozenlist" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/63/e7/fa1a8c00e2c54b05dc8cb5d1439f627f7c267874e3f7bb047146116020f9/aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a", size = 7678653, upload-time = "2025-04-21T09:43:09.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/d2/5bc436f42bf4745c55f33e1e6a2d69e77075d3e768e3d1a34f96ee5298aa/aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2", size = 706671, upload-time = "2025-04-21T09:41:28.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/d0/2dbabecc4e078c0474abb40536bbde717fb2e39962f41c5fc7a216b18ea7/aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508", size = 466169, upload-time = "2025-04-21T09:41:29.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/84/19edcf0b22933932faa6e0be0d933a27bd173da02dc125b7354dff4d8da4/aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e", size = 457554, upload-time = "2025-04-21T09:41:31.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d0/e8d1f034ae5624a0f21e4fb3feff79342ce631f3a4d26bd3e58b31ef033b/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f", size = 1690154, upload-time = "2025-04-21T09:41:33.541Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/de/2f9dbe2ac6f38f8495562077131888e0d2897e3798a0ff3adda766b04a34/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f", size = 1733402, upload-time = "2025-04-21T09:41:35.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/04/bd2870e1e9aef990d14b6df2a695f17807baf5c85a4c187a492bda569571/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec", size = 1783958, upload-time = "2025-04-21T09:41:37.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/06/4203ffa2beb5bedb07f0da0f79b7d9039d1c33f522e0d1a2d5b6218e6f2e/aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6", size = 1695288, upload-time = "2025-04-21T09:41:39.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/b2/e2285dda065d9f29ab4b23d8bcc81eb881db512afb38a3f5247b191be36c/aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009", size = 1618871, upload-time = "2025-04-21T09:41:41.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/e0/88f2987885d4b646de2036f7296ebea9268fdbf27476da551c1a7c158bc0/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4", size = 1646262, upload-time = "2025-04-21T09:41:44.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/19/4d2da508b4c587e7472a032290b2981f7caeca82b4354e19ab3df2f51d56/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9", size = 1677431, upload-time = "2025-04-21T09:41:46.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/ae/047473ea50150a41440f3265f53db1738870b5a1e5406ece561ca61a3bf4/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb", size = 1637430, upload-time = "2025-04-21T09:41:47.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/32/c6d1e3748077ce7ee13745fae33e5cb1dac3e3b8f8787bf738a93c94a7d2/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda", size = 1703342, upload-time = "2025-04-21T09:41:50.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/1d/a3b57bfdbe285f0d45572d6d8f534fd58761da3e9cbc3098372565005606/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1", size = 1740600, upload-time = "2025-04-21T09:41:52.111Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/71/f9cd2fed33fa2b7ce4d412fb7876547abb821d5b5520787d159d0748321d/aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea", size = 1695131, upload-time = "2025-04-21T09:41:53.94Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/97/d1248cd6d02b9de6aa514793d0dcb20099f0ec47ae71a933290116c070c5/aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8", size = 412442, upload-time = "2025-04-21T09:41:55.689Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/9a/e34e65506e06427b111e19218a99abf627638a9703f4b8bcc3e3021277ed/aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8", size = 439444, upload-time = "2025-04-21T09:41:57.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/18/be8b5dd6b9cf1b2172301dbed28e8e5e878ee687c21947a6c81d6ceaa15d/aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811", size = 699833, upload-time = "2025-04-21T09:42:00.298Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/84/ecdc68e293110e6f6f6d7b57786a77555a85f70edd2b180fb1fafaff361a/aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804", size = 462774, upload-time = "2025-04-21T09:42:02.015Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/85/f07718cca55884dad83cc2433746384d267ee970e91f0dcc75c6d5544079/aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd", size = 454429, upload-time = "2025-04-21T09:42:03.728Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/02/7f669c3d4d39810db8842c4e572ce4fe3b3a9b82945fdd64affea4c6947e/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c", size = 1670283, upload-time = "2025-04-21T09:42:06.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/79/b82a12f67009b377b6c07a26bdd1b81dab7409fc2902d669dbfa79e5ac02/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118", size = 1717231, upload-time = "2025-04-21T09:42:07.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/38/d5a1f28c3904a840642b9a12c286ff41fc66dfa28b87e204b1f242dbd5e6/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1", size = 1769621, upload-time = "2025-04-21T09:42:09.855Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/2d/deb3749ba293e716b5714dda06e257f123c5b8679072346b1eb28b766a0b/aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000", size = 1678667, upload-time = "2025-04-21T09:42:11.741Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/a8/04b6e11683a54e104b984bd19a9790eb1ae5f50968b601bb202d0406f0ff/aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137", size = 1601592, upload-time = "2025-04-21T09:42:14.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/9d/c33305ae8370b789423623f0e073d09ac775cd9c831ac0f11338b81c16e0/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93", size = 1621679, upload-time = "2025-04-21T09:42:16.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/45/8e9a27fff0538173d47ba60362823358f7a5f1653c6c30c613469f94150e/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3", size = 1656878, upload-time = "2025-04-21T09:42:18.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/5b/8c5378f10d7a5a46b10cb9161a3aac3eeae6dba54ec0f627fc4ddc4f2e72/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8", size = 1620509, upload-time = "2025-04-21T09:42:20.141Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/2f/99dee7bd91c62c5ff0aa3c55f4ae7e1bc99c6affef780d7777c60c5b3735/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2", size = 1680263, upload-time = "2025-04-21T09:42:21.993Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0a/378745e4ff88acb83e2d5c884a4fe993a6e9f04600a4560ce0e9b19936e3/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261", size = 1715014, upload-time = "2025-04-21T09:42:23.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/0b/b5524b3bb4b01e91bc4323aad0c2fcaebdf2f1b4d2eb22743948ba364958/aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7", size = 1666614, upload-time = "2025-04-21T09:42:25.764Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/b7/3d7b036d5a4ed5a4c704e0754afe2eef24a824dfab08e6efbffb0f6dd36a/aiohttp-3.11.18-cp313-cp313-win32.whl", hash = "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78", size = 411358, upload-time = "2025-04-21T09:42:27.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/3c/143831b32cd23b5263a995b2a1794e10aa42f8a895aae5074c20fda36c07/aiohttp-3.11.18-cp313-cp313-win_amd64.whl", hash = "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01", size = 437658, upload-time = "2025-04-21T09:42:29.209Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aioredis"
|
||||
version = "2.0.1"
|
||||
@@ -93,18 +35,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/a9/0da089c3ae7a31cbcd2dcf0214f6f571e1295d292b6139e2bac68ec081d0/aioredis-2.0.1-py3-none-any.whl", hash = "sha256:9ac0d0b3b485d293b8ca1987e6de8658d7dafcca1cddfcd1d506cae8cdebfdd6", size = 71243, upload-time = "2021-12-27T20:28:16.36Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "frozenlist" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "amqp"
|
||||
version = "5.3.1"
|
||||
@@ -135,6 +65,25 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/71/aee71b836e9ee2741d5694b80d74bfc7c8cd5dbdf7a9f3035fcf80d792b1/ansi2html-1.9.2-py3-none-any.whl", hash = "sha256:dccb75aa95fb018e5d299be2b45f802952377abfdce0504c17a6ee6ef0a420c5", size = 17614, upload-time = "2024-06-22T17:33:21.852Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.102.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "docstring-parser" },
|
||||
{ name = "httpx" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/47/cb2a71f70431fb09af4db83e3ea89eb2dd8e0e348d27af53ed32e6c599dd/anthropic-0.102.0.tar.gz", hash = "sha256:96f747cad11886c4ae12d4080131b94eebd68b202bd2190fe27959031bb1fa9c", size = 763697, upload-time = "2026-05-13T18:12:41.624Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/75/0f6c603594876413bc858a00e7cc0d80a0cc14edf5c7b959a3ea6ec45e44/anthropic-0.102.0-py3-none-any.whl", hash = "sha256:ab96540bbd4b0f36564252d955a86f8abbe4f00944a24bc9931acc9b139bab6f", size = 763070, upload-time = "2026-05-13T18:12:43.474Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.9.0"
|
||||
@@ -193,6 +142,7 @@ dependencies = [
|
||||
{ name = "aiofile" },
|
||||
{ name = "aioredis" },
|
||||
{ name = "ansi2html" },
|
||||
{ name = "anthropic" },
|
||||
{ name = "celery" },
|
||||
{ name = "chromadb" },
|
||||
{ name = "e2b-code-interpreter" },
|
||||
@@ -201,7 +151,6 @@ dependencies = [
|
||||
{ name = "icecream" },
|
||||
{ name = "ipykernel" },
|
||||
{ name = "jupyter-client" },
|
||||
{ name = "litellm" },
|
||||
{ name = "loguru" },
|
||||
{ name = "matplotlib" },
|
||||
{ name = "nbformat" },
|
||||
@@ -215,6 +164,7 @@ dependencies = [
|
||||
{ name = "pypandoc-binary" },
|
||||
{ name = "rank-bm25" },
|
||||
{ name = "redis" },
|
||||
{ name = "requests" },
|
||||
{ name = "scikit-learn" },
|
||||
{ name = "scipy" },
|
||||
{ name = "seaborn" },
|
||||
@@ -237,6 +187,7 @@ requires-dist = [
|
||||
{ name = "aiofile", specifier = ">=3.9.0" },
|
||||
{ name = "aioredis", specifier = ">=2.0.1" },
|
||||
{ name = "ansi2html", specifier = ">=1.9.2" },
|
||||
{ name = "anthropic", specifier = ">=0.40.0" },
|
||||
{ name = "celery", specifier = ">=5.4.0" },
|
||||
{ name = "chromadb", specifier = ">=0.4.0" },
|
||||
{ name = "e2b-code-interpreter", specifier = ">=1.0.5" },
|
||||
@@ -245,7 +196,6 @@ requires-dist = [
|
||||
{ name = "icecream", specifier = ">=2.1.4" },
|
||||
{ name = "ipykernel", specifier = ">=6.29.5" },
|
||||
{ name = "jupyter-client", specifier = ">=8.6.3" },
|
||||
{ name = "litellm", specifier = ">=1.69.0" },
|
||||
{ name = "loguru", specifier = ">=0.7.3" },
|
||||
{ name = "matplotlib", specifier = ">=3.10.1" },
|
||||
{ name = "nbformat", specifier = ">=5.10.4" },
|
||||
@@ -259,6 +209,7 @@ requires-dist = [
|
||||
{ name = "pypandoc-binary", specifier = ">=1.15" },
|
||||
{ name = "rank-bm25", specifier = ">=0.2.2" },
|
||||
{ name = "redis", specifier = ">=5.2.1" },
|
||||
{ name = "requests", specifier = ">=2.32.0" },
|
||||
{ name = "scikit-learn", specifier = ">=1.6.1" },
|
||||
{ name = "scipy", specifier = ">=1.15.2" },
|
||||
{ name = "seaborn", specifier = ">=0.13.2" },
|
||||
@@ -757,6 +708,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/68/1b/e0a87d256e40e8c888847551b20a017a6b98139178505dc7ffb96f04e954/dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86", size = 313632, upload-time = "2024-10-05T20:14:57.687Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "durationpy"
|
||||
version = "0.10"
|
||||
@@ -923,66 +883,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/27/45f8957c3132917f91aaa56b700bcfc2396be1253f685bd5c68529b6f610/fonttools-4.57.0-py3-none-any.whl", hash = "sha256:3122c604a675513c68bd24c6a8f9091f1c2376d18e8f5fe5a101746c81b3e98f", size = 1093605, upload-time = "2025-04-03T11:07:11.341Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "frozenlist"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ee/f4/d744cba2da59b5c1d88823cf9e8a6c74e4659e2b27604ed973be2a0bf5ab/frozenlist-1.6.0.tar.gz", hash = "sha256:b99655c32c1c8e06d111e7f41c06c29a5318cb1835df23a45518e02a47c63b68", size = 42831, upload-time = "2025-04-17T22:38:53.099Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/8a/289b7d0de2fbac832ea80944d809759976f661557a38bb8e77db5d9f79b7/frozenlist-1.6.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c5b9e42ace7d95bf41e19b87cec8f262c41d3510d8ad7514ab3862ea2197bfb1", size = 160193, upload-time = "2025-04-17T22:36:47.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/80/2fd17d322aec7f430549f0669f599997174f93ee17929ea5b92781ec902c/frozenlist-1.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ca9973735ce9f770d24d5484dcb42f68f135351c2fc81a7a9369e48cf2998a29", size = 123831, upload-time = "2025-04-17T22:36:49.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/06/f5812da431273f78c6543e0b2f7de67dfd65eb0a433978b2c9c63d2205e4/frozenlist-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6ac40ec76041c67b928ca8aaffba15c2b2ee3f5ae8d0cb0617b5e63ec119ca25", size = 121862, upload-time = "2025-04-17T22:36:51.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/31/9e61c6b5fc493cf24d54881731204d27105234d09878be1a5983182cc4a5/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b7a8a3180dfb280eb044fdec562f9b461614c0ef21669aea6f1d3dac6ee576", size = 316361, upload-time = "2025-04-17T22:36:53.402Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/55/22ca9362d4f0222324981470fd50192be200154d51509ee6eb9baa148e96/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c444d824e22da6c9291886d80c7d00c444981a72686e2b59d38b285617cb52c8", size = 307115, upload-time = "2025-04-17T22:36:55.016Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/39/4fff42920a57794881e7bb3898dc7f5f539261711ea411b43bba3cde8b79/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bb52c8166499a8150bfd38478248572c924c003cbb45fe3bcd348e5ac7c000f9", size = 322505, upload-time = "2025-04-17T22:36:57.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/f2/88c41f374c1e4cf0092a5459e5f3d6a1e17ed274c98087a76487783df90c/frozenlist-1.6.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b35298b2db9c2468106278537ee529719228950a5fdda686582f68f247d1dc6e", size = 322666, upload-time = "2025-04-17T22:36:58.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/51/034eeb75afdf3fd03997856195b500722c0b1a50716664cde64e28299c4b/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d108e2d070034f9d57210f22fefd22ea0d04609fc97c5f7f5a686b3471028590", size = 302119, upload-time = "2025-04-17T22:37:00.512Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/a6/564ecde55ee633270a793999ef4fd1d2c2b32b5a7eec903b1012cb7c5143/frozenlist-1.6.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e1be9111cb6756868ac242b3c2bd1f09d9aea09846e4f5c23715e7afb647103", size = 316226, upload-time = "2025-04-17T22:37:02.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/c8/6c0682c32377f402b8a6174fb16378b683cf6379ab4d2827c580892ab3c7/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:94bb451c664415f02f07eef4ece976a2c65dcbab9c2f1705b7031a3a75349d8c", size = 312788, upload-time = "2025-04-17T22:37:03.578Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/b8/10fbec38f82c5d163ca1750bfff4ede69713badf236a016781cf1f10a0f0/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d1a686d0b0949182b8faddea596f3fc11f44768d1f74d4cad70213b2e139d821", size = 325914, upload-time = "2025-04-17T22:37:05.213Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/ca/2bf4f3a1bd40cdedd301e6ecfdbb291080d5afc5f9ce350c0739f773d6b9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ea8e59105d802c5a38bdbe7362822c522230b3faba2aa35c0fa1765239b7dd70", size = 305283, upload-time = "2025-04-17T22:37:06.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/64/20cc13ccf94abc2a1f482f74ad210703dc78a590d0b805af1c9aa67f76f9/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:abc4e880a9b920bc5020bf6a431a6bb40589d9bca3975c980495f63632e8382f", size = 319264, upload-time = "2025-04-17T22:37:08.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/ff/86c6a2bbe98cfc231519f5e6d712a0898488ceac804a917ce014f32e68f6/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9a79713adfe28830f27a3c62f6b5406c37376c892b05ae070906f07ae4487046", size = 326482, upload-time = "2025-04-17T22:37:10.196Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/da/8e381f66367d79adca245d1d71527aac774e30e291d41ef161ce2d80c38e/frozenlist-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a0318c2068e217a8f5e3b85e35899f5a19e97141a45bb925bb357cfe1daf770", size = 318248, upload-time = "2025-04-17T22:37:12.284Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/24/1a1976563fb476ab6f0fa9fefaac7616a4361dbe0461324f9fd7bf425dbe/frozenlist-1.6.0-cp312-cp312-win32.whl", hash = "sha256:853ac025092a24bb3bf09ae87f9127de9fe6e0c345614ac92536577cf956dfcc", size = 115161, upload-time = "2025-04-17T22:37:13.902Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/2e/fb4ed62a65f8cd66044706b1013f0010930d8cbb0729a2219561ea075434/frozenlist-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:2bdfe2d7e6c9281c6e55523acd6c2bf77963cb422fdc7d142fb0cb6621b66878", size = 120548, upload-time = "2025-04-17T22:37:15.326Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/e5/04c7090c514d96ca00887932417f04343ab94904a56ab7f57861bf63652d/frozenlist-1.6.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d7fb014fe0fbfee3efd6a94fc635aeaa68e5e1720fe9e57357f2e2c6e1a647e", size = 158182, upload-time = "2025-04-17T22:37:16.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/8f/60d0555c61eec855783a6356268314d204137f5e0c53b59ae2fc28938c99/frozenlist-1.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01bcaa305a0fdad12745502bfd16a1c75b14558dabae226852f9159364573117", size = 122838, upload-time = "2025-04-17T22:37:18.352Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/a7/d0ec890e3665b4b3b7c05dc80e477ed8dc2e2e77719368e78e2cd9fec9c8/frozenlist-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b314faa3051a6d45da196a2c495e922f987dc848e967d8cfeaee8a0328b1cd4", size = 120980, upload-time = "2025-04-17T22:37:19.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/19/9b355a5e7a8eba903a008579964192c3e427444752f20b2144b10bb336df/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da62fecac21a3ee10463d153549d8db87549a5e77eefb8c91ac84bb42bb1e4e3", size = 305463, upload-time = "2025-04-17T22:37:21.328Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/8d/5b4c758c2550131d66935ef2fa700ada2461c08866aef4229ae1554b93ca/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1eb89bf3454e2132e046f9599fbcf0a4483ed43b40f545551a39316d0201cd1", size = 297985, upload-time = "2025-04-17T22:37:23.55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/2c/537ec09e032b5865715726b2d1d9813e6589b571d34d01550c7aeaad7e53/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18689b40cb3936acd971f663ccb8e2589c45db5e2c5f07e0ec6207664029a9c", size = 311188, upload-time = "2025-04-17T22:37:25.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/31/2f/1aa74b33f74d54817055de9a4961eff798f066cdc6f67591905d4fc82a84/frozenlist-1.6.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e67ddb0749ed066b1a03fba812e2dcae791dd50e5da03be50b6a14d0c1a9ee45", size = 311874, upload-time = "2025-04-17T22:37:26.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/f0/cfec18838f13ebf4b37cfebc8649db5ea71a1b25dacd691444a10729776c/frozenlist-1.6.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc5e64626e6682638d6e44398c9baf1d6ce6bc236d40b4b57255c9d3f9761f1f", size = 291897, upload-time = "2025-04-17T22:37:28.958Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/a5/deb39325cbbea6cd0a46db8ccd76150ae2fcbe60d63243d9df4a0b8c3205/frozenlist-1.6.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:437cfd39564744ae32ad5929e55b18ebd88817f9180e4cc05e7d53b75f79ce85", size = 305799, upload-time = "2025-04-17T22:37:30.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/22/6ddec55c5243a59f605e4280f10cee8c95a449f81e40117163383829c241/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62dd7df78e74d924952e2feb7357d826af8d2f307557a779d14ddf94d7311be8", size = 302804, upload-time = "2025-04-17T22:37:32.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/b7/d9ca9bab87f28855063c4d202936800219e39db9e46f9fb004d521152623/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a66781d7e4cddcbbcfd64de3d41a61d6bdde370fc2e38623f30b2bd539e84a9f", size = 316404, upload-time = "2025-04-17T22:37:34.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/3a/1255305db7874d0b9eddb4fe4a27469e1fb63720f1fc6d325a5118492d18/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:482fe06e9a3fffbcd41950f9d890034b4a54395c60b5e61fae875d37a699813f", size = 295572, upload-time = "2025-04-17T22:37:36.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/f2/8d38eeee39a0e3a91b75867cc102159ecccf441deb6ddf67be96d3410b84/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e4f9373c500dfc02feea39f7a56e4f543e670212102cc2eeb51d3a99c7ffbde6", size = 307601, upload-time = "2025-04-17T22:37:37.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/04/80ec8e6b92f61ef085422d7b196822820404f940950dde5b2e367bede8bc/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e69bb81de06827147b7bfbaeb284d85219fa92d9f097e32cc73675f279d70188", size = 314232, upload-time = "2025-04-17T22:37:39.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/58/93b41fb23e75f38f453ae92a2f987274c64637c450285577bd81c599b715/frozenlist-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7613d9977d2ab4a9141dde4a149f4357e4065949674c5649f920fec86ecb393e", size = 308187, upload-time = "2025-04-17T22:37:41.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/a2/e64df5c5aa36ab3dee5a40d254f3e471bb0603c225f81664267281c46a2d/frozenlist-1.6.0-cp313-cp313-win32.whl", hash = "sha256:4def87ef6d90429f777c9d9de3961679abf938cb6b7b63d4a7eb8a268babfce4", size = 114772, upload-time = "2025-04-17T22:37:43.132Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/77/fead27441e749b2d574bb73d693530d59d520d4b9e9679b8e3cb779d37f2/frozenlist-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:37a8a52c3dfff01515e9bbbee0e6063181362f9de3db2ccf9bc96189b557cbfd", size = 119847, upload-time = "2025-04-17T22:37:45.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/bd/cc6d934991c1e5d9cafda83dfdc52f987c7b28343686aef2e58a9cf89f20/frozenlist-1.6.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:46138f5a0773d064ff663d273b309b696293d7a7c00a0994c5c13a5078134b64", size = 174937, upload-time = "2025-04-17T22:37:46.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/a2/daf945f335abdbfdd5993e9dc348ef4507436936ab3c26d7cfe72f4843bf/frozenlist-1.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f88bc0a2b9c2a835cb888b32246c27cdab5740059fb3688852bf91e915399b91", size = 136029, upload-time = "2025-04-17T22:37:48.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/65/4c3145f237a31247c3429e1c94c384d053f69b52110a0d04bfc8afc55fb2/frozenlist-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:777704c1d7655b802c7850255639672e90e81ad6fa42b99ce5ed3fbf45e338dd", size = 134831, upload-time = "2025-04-17T22:37:50.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/38/03d316507d8dea84dfb99bdd515ea245628af964b2bf57759e3c9205cc5e/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85ef8d41764c7de0dcdaf64f733a27352248493a85a80661f3c678acd27e31f2", size = 392981, upload-time = "2025-04-17T22:37:52.558Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/37/02/46285ef9828f318ba400a51d5bb616ded38db8466836a9cfa39f3903260b/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:da5cb36623f2b846fb25009d9d9215322318ff1c63403075f812b3b2876c8506", size = 371999, upload-time = "2025-04-17T22:37:54.092Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/64/1212fea37a112c3c5c05bfb5f0a81af4836ce349e69be75af93f99644da9/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbb56587a16cf0fb8acd19e90ff9924979ac1431baea8681712716a8337577b0", size = 392200, upload-time = "2025-04-17T22:37:55.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/ce/9a6ea1763e3366e44a5208f76bf37c76c5da570772375e4d0be85180e588/frozenlist-1.6.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6154c3ba59cda3f954c6333025369e42c3acd0c6e8b6ce31eb5c5b8116c07e0", size = 390134, upload-time = "2025-04-17T22:37:57.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/36/939738b0b495b2c6d0c39ba51563e453232813042a8d908b8f9544296c29/frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e8246877afa3f1ae5c979fe85f567d220f86a50dc6c493b9b7d8191181ae01e", size = 365208, upload-time = "2025-04-17T22:37:59.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/8b/939e62e93c63409949c25220d1ba8e88e3960f8ef6a8d9ede8f94b459d27/frozenlist-1.6.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b0f6cce16306d2e117cf9db71ab3a9e8878a28176aeaf0dbe35248d97b28d0c", size = 385548, upload-time = "2025-04-17T22:38:01.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/38/22d2873c90102e06a7c5a3a5b82ca47e393c6079413e8a75c72bff067fa8/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1b8e8cd8032ba266f91136d7105706ad57770f3522eac4a111d77ac126a25a9b", size = 391123, upload-time = "2025-04-17T22:38:03.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/78/63aaaf533ee0701549500f6d819be092c6065cb5c577edb70c09df74d5d0/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e2ada1d8515d3ea5378c018a5f6d14b4994d4036591a52ceaf1a1549dec8e1ad", size = 394199, upload-time = "2025-04-17T22:38:04.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/45/71a6b48981d429e8fbcc08454dc99c4c2639865a646d549812883e9c9dd3/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:cdb2c7f071e4026c19a3e32b93a09e59b12000751fc9b0b7758da899e657d215", size = 373854, upload-time = "2025-04-17T22:38:06.576Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/f3/dbf2a5e11736ea81a66e37288bf9f881143a7822b288a992579ba1b4204d/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:03572933a1969a6d6ab509d509e5af82ef80d4a5d4e1e9f2e1cdd22c77a3f4d2", size = 395412, upload-time = "2025-04-17T22:38:08.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/f1/c63166806b331f05104d8ea385c4acd511598568b1f3e4e8297ca54f2676/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:77effc978947548b676c54bbd6a08992759ea6f410d4987d69feea9cd0919911", size = 394936, upload-time = "2025-04-17T22:38:10.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/ea/4f3e69e179a430473eaa1a75ff986526571215fefc6b9281cdc1f09a4eb8/frozenlist-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a2bda8be77660ad4089caf2223fdbd6db1858462c4b85b67fbfa22102021e497", size = 391459, upload-time = "2025-04-17T22:38:11.826Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/c3/0fc2c97dea550df9afd072a37c1e95421652e3206bbeaa02378b24c2b480/frozenlist-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:a4d96dc5bcdbd834ec6b0f91027817214216b5b30316494d2b1aebffb87c534f", size = 128797, upload-time = "2025-04-17T22:38:14.013Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/f5/79c9320c5656b1965634fe4be9c82b12a3305bdbc58ad9cb941131107b20/frozenlist-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:e18036cb4caa17ea151fd5f3d70be9d354c99eb8cf817a3ccde8a7873b074348", size = 134709, upload-time = "2025-04-17T22:38:15.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/3e/b04a0adda73bd52b390d730071c0d577073d3d26740ee1bad25c3ad0f37b/frozenlist-1.6.0-py3-none-any.whl", hash = "sha256:535eec9987adb04701266b92745d6cdcef2e77669299359c3009c3404dd5d191", size = 12404, upload-time = "2025-04-17T22:38:51.668Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsspec"
|
||||
version = "2025.3.2"
|
||||
@@ -1538,28 +1438,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/70/05b685ea2dffcb2adbf3cdcea5d8865b7bc66f67249084cf845012a0ff13/kubernetes-35.0.0-py2.py3-none-any.whl", hash = "sha256:39e2b33b46e5834ef6c3985ebfe2047ab39135d41de51ce7641a7ca5b372a13d", size = 2017602, upload-time = "2026-01-16T01:05:25.991Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litellm"
|
||||
version = "1.69.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "click" },
|
||||
{ name = "httpx" },
|
||||
{ name = "importlib-metadata" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "jsonschema" },
|
||||
{ name = "openai" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "tiktoken" },
|
||||
{ name = "tokenizers" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f1/55/8d27d9c39fc092174ea274a1e5f7e237078b0959f3b9d70448d598551f97/litellm-1.69.0.tar.gz", hash = "sha256:9f37f5fedef87fa13ebfe89a8c9c484d609ffcbd6b82f2d1bb6e1f78761be335", size = 7365657, upload-time = "2025-05-11T01:04:57.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/8e/7a3812586581304e0aa5e8555bf21f00c6b576fe98ce17ed42d66caa2c47/litellm-1.69.0-py3-none-any.whl", hash = "sha256:7fe2745dfd991bee377922b53e25f8cc7a68fc89e3c5dbbdc1d0bacb942b5d78", size = 7722378, upload-time = "2025-05-11T01:04:53.941Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "llvmlite"
|
||||
version = "0.44.0"
|
||||
@@ -1790,66 +1668,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.4.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/2c/e367dfb4c6538614a0c9453e510d75d66099edf1c4e69da1b5ce691a1931/multidict-6.4.3.tar.gz", hash = "sha256:3ada0b058c9f213c5f95ba301f922d402ac234f1111a7d8fd70f1b99f3c281ec", size = 89372, upload-time = "2025-04-10T22:20:17.956Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/bb/3abdaf8fe40e9226ce8a2ba5ecf332461f7beec478a455d6587159f1bf92/multidict-6.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1f1c2f58f08b36f8475f3ec6f5aeb95270921d418bf18f90dffd6be5c7b0e676", size = 64019, upload-time = "2025-04-10T22:18:23.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/b5/1b2e8de8217d2e89db156625aa0fe4a6faad98972bfe07a7b8c10ef5dd6b/multidict-6.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:26ae9ad364fc61b936fb7bf4c9d8bd53f3a5b4417142cd0be5c509d6f767e2f1", size = 37925, upload-time = "2025-04-10T22:18:24.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/e2/3ca91c112644a395c8eae017144c907d173ea910c913ff8b62549dcf0bbf/multidict-6.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:659318c6c8a85f6ecfc06b4e57529e5a78dfdd697260cc81f683492ad7e9435a", size = 37008, upload-time = "2025-04-10T22:18:26.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/23/79bc78146c7ac8d1ac766b2770ca2e07c2816058b8a3d5da6caed8148637/multidict-6.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1eb72c741fd24d5a28242ce72bb61bc91f8451877131fa3fe930edb195f7054", size = 224374, upload-time = "2025-04-10T22:18:27.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/35/77950ed9ebd09136003a85c1926ba42001ca5be14feb49710e4334ee199b/multidict-6.4.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cd06d88cb7398252284ee75c8db8e680aa0d321451132d0dba12bc995f0adcc", size = 230869, upload-time = "2025-04-10T22:18:29.162Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/97/2a33c6e7d90bc116c636c14b2abab93d6521c0c052d24bfcc231cbf7f0e7/multidict-6.4.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4543d8dc6470a82fde92b035a92529317191ce993533c3c0c68f56811164ed07", size = 231949, upload-time = "2025-04-10T22:18:30.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/56/ce/e9b5d9fcf854f61d6686ada7ff64893a7a5523b2a07da6f1265eaaea5151/multidict-6.4.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:30a3ebdc068c27e9d6081fca0e2c33fdf132ecea703a72ea216b81a66860adde", size = 231032, upload-time = "2025-04-10T22:18:32.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ac/7ced59dcdfeddd03e601edb05adff0c66d81ed4a5160c443e44f2379eef0/multidict-6.4.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b038f10e23f277153f86f95c777ba1958bcd5993194fda26a1d06fae98b2f00c", size = 223517, upload-time = "2025-04-10T22:18:33.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/e6/325ed9055ae4e085315193a1b58bdb4d7fc38ffcc1f4975cfca97d015e17/multidict-6.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c605a2b2dc14282b580454b9b5d14ebe0668381a3a26d0ac39daa0ca115eb2ae", size = 216291, upload-time = "2025-04-10T22:18:34.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/84/eeee6d477dd9dcb7691c3bb9d08df56017f5dd15c730bcc9383dcf201cf4/multidict-6.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8bd2b875f4ca2bb527fe23e318ddd509b7df163407b0fb717df229041c6df5d3", size = 228982, upload-time = "2025-04-10T22:18:36.443Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/94/4d1f3e74e7acf8b0c85db350e012dcc61701cd6668bc2440bb1ecb423c90/multidict-6.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c2e98c840c9c8e65c0e04b40c6c5066c8632678cd50c8721fdbcd2e09f21a507", size = 226823, upload-time = "2025-04-10T22:18:37.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/f0/1e54b95bda7cd01080e5732f9abb7b76ab5cc795b66605877caeb2197476/multidict-6.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66eb80dd0ab36dbd559635e62fba3083a48a252633164857a1d1684f14326427", size = 222714, upload-time = "2025-04-10T22:18:39.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/a2/f6cbca875195bd65a3e53b37ab46486f3cc125bdeab20eefe5042afa31fb/multidict-6.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c23831bdee0a2a3cf21be057b5e5326292f60472fb6c6f86392bbf0de70ba731", size = 233739, upload-time = "2025-04-10T22:18:41.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/68/9891f4d2b8569554723ddd6154375295f789dc65809826c6fb96a06314fd/multidict-6.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1535cec6443bfd80d028052e9d17ba6ff8a5a3534c51d285ba56c18af97e9713", size = 230809, upload-time = "2025-04-10T22:18:42.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/72/a7be29ba1e87e4fc5ceb44dabc7940b8005fd2436a332a23547709315f70/multidict-6.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3b73e7227681f85d19dec46e5b881827cd354aabe46049e1a61d2f9aaa4e285a", size = 226934, upload-time = "2025-04-10T22:18:44.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/c1/259386a9ad6840ff7afc686da96808b503d152ac4feb3a96c651dc4f5abf/multidict-6.4.3-cp312-cp312-win32.whl", hash = "sha256:8eac0c49df91b88bf91f818e0a24c1c46f3622978e2c27035bfdca98e0e18124", size = 35242, upload-time = "2025-04-10T22:18:46.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/24/c8fdff4f924d37225dc0c56a28b1dca10728fc2233065fafeb27b4b125be/multidict-6.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:11990b5c757d956cd1db7cb140be50a63216af32cd6506329c2c59d732d802db", size = 38635, upload-time = "2025-04-10T22:18:47.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/4b/86fd786d03915c6f49998cf10cd5fe6b6ac9e9a071cb40885d2e080fb90d/multidict-6.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7a76534263d03ae0cfa721fea40fd2b5b9d17a6f85e98025931d41dc49504474", size = 63831, upload-time = "2025-04-10T22:18:48.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/05/9b51fdf7aef2563340a93be0a663acba2c428c4daeaf3960d92d53a4a930/multidict-6.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:805031c2f599eee62ac579843555ed1ce389ae00c7e9f74c2a1b45e0564a88dd", size = 37888, upload-time = "2025-04-10T22:18:50.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/43/53fc25394386c911822419b522181227ca450cf57fea76e6188772a1bd91/multidict-6.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c56c179839d5dcf51d565132185409d1d5dd8e614ba501eb79023a6cab25576b", size = 36852, upload-time = "2025-04-10T22:18:51.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/68/7b99c751e822467c94a235b810a2fd4047d4ecb91caef6b5c60116991c4b/multidict-6.4.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c64f4ddb3886dd8ab71b68a7431ad4aa01a8fa5be5b11543b29674f29ca0ba3", size = 223644, upload-time = "2025-04-10T22:18:52.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/1b/d458d791e4dd0f7e92596667784fbf99e5c8ba040affe1ca04f06b93ae92/multidict-6.4.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3002a856367c0b41cad6784f5b8d3ab008eda194ed7864aaa58f65312e2abcac", size = 230446, upload-time = "2025-04-10T22:18:54.509Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/46/9793378d988905491a7806d8987862dc5a0bae8a622dd896c4008c7b226b/multidict-6.4.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d75e621e7d887d539d6e1d789f0c64271c250276c333480a9e1de089611f790", size = 231070, upload-time = "2025-04-10T22:18:56.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/b8/b127d3e1f8dd2a5bf286b47b24567ae6363017292dc6dec44656e6246498/multidict-6.4.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:995015cf4a3c0d72cbf453b10a999b92c5629eaf3a0c3e1efb4b5c1f602253bb", size = 229956, upload-time = "2025-04-10T22:18:59.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/93/f70a4c35b103fcfe1443059a2bb7f66e5c35f2aea7804105ff214f566009/multidict-6.4.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b0fabae7939d09d7d16a711468c385272fa1b9b7fb0d37e51143585d8e72e0", size = 222599, upload-time = "2025-04-10T22:19:00.657Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/8c/e28e0eb2fe34921d6aa32bfc4ac75b09570b4d6818cc95d25499fe08dc1d/multidict-6.4.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61ed4d82f8a1e67eb9eb04f8587970d78fe7cddb4e4d6230b77eda23d27938f9", size = 216136, upload-time = "2025-04-10T22:19:02.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f5/fbc81f866585b05f89f99d108be5d6ad170e3b6c4d0723d1a2f6ba5fa918/multidict-6.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:062428944a8dc69df9fdc5d5fc6279421e5f9c75a9ee3f586f274ba7b05ab3c8", size = 228139, upload-time = "2025-04-10T22:19:04.151Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/ba/7d196bad6b85af2307d81f6979c36ed9665f49626f66d883d6c64d156f78/multidict-6.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b90e27b4674e6c405ad6c64e515a505c6d113b832df52fdacb6b1ffd1fa9a1d1", size = 226251, upload-time = "2025-04-10T22:19:06.117Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/e2/fae46a370dce79d08b672422a33df721ec8b80105e0ea8d87215ff6b090d/multidict-6.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7d50d4abf6729921e9613d98344b74241572b751c6b37feed75fb0c37bd5a817", size = 221868, upload-time = "2025-04-10T22:19:07.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/20/bbc9a3dec19d5492f54a167f08546656e7aef75d181d3d82541463450e88/multidict-6.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:43fe10524fb0a0514be3954be53258e61d87341008ce4914f8e8b92bee6f875d", size = 233106, upload-time = "2025-04-10T22:19:09.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8d/f30ae8f5ff7a2461177f4d8eb0d8f69f27fb6cfe276b54ec4fd5a282d918/multidict-6.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:236966ca6c472ea4e2d3f02f6673ebfd36ba3f23159c323f5a496869bc8e47c9", size = 230163, upload-time = "2025-04-10T22:19:11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/e9/2833f3c218d3c2179f3093f766940ded6b81a49d2e2f9c46ab240d23dfec/multidict-6.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:422a5ec315018e606473ba1f5431e064cf8b2a7468019233dcf8082fabad64c8", size = 225906, upload-time = "2025-04-10T22:19:12.875Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/31/6edab296ac369fd286b845fa5dd4c409e63bc4655ed8c9510fcb477e9ae9/multidict-6.4.3-cp313-cp313-win32.whl", hash = "sha256:f901a5aace8e8c25d78960dcc24c870c8d356660d3b49b93a78bf38eb682aac3", size = 35238, upload-time = "2025-04-10T22:19:14.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/57/2c0167a1bffa30d9a1383c3dab99d8caae985defc8636934b5668830d2ef/multidict-6.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:1c152c49e42277bc9a2f7b78bd5fa10b13e88d1b0328221e7aef89d5c60a99a5", size = 38799, upload-time = "2025-04-10T22:19:15.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/13/2ead63b9ab0d2b3080819268acb297bd66e238070aa8d42af12b08cbee1c/multidict-6.4.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:be8751869e28b9c0d368d94f5afcb4234db66fe8496144547b4b6d6a0645cfc6", size = 68642, upload-time = "2025-04-10T22:19:17.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/45/f1a751e1eede30c23951e2ae274ce8fad738e8a3d5714be73e0a41b27b16/multidict-6.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d4b31f8a68dccbcd2c0ea04f0e014f1defc6b78f0eb8b35f2265e8716a6df0c", size = 40028, upload-time = "2025-04-10T22:19:19.465Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/29/fcc53e886a2cc5595cc4560df333cb9630257bda65003a7eb4e4e0d8f9c1/multidict-6.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:032efeab3049e37eef2ff91271884303becc9e54d740b492a93b7e7266e23756", size = 39424, upload-time = "2025-04-10T22:19:20.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/f0/056c81119d8b88703971f937b371795cab1407cd3c751482de5bfe1a04a9/multidict-6.4.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e78006af1a7c8a8007e4f56629d7252668344442f66982368ac06522445e375", size = 226178, upload-time = "2025-04-10T22:19:22.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/79/3b7e5fea0aa80583d3a69c9d98b7913dfd4fbc341fb10bb2fb48d35a9c21/multidict-6.4.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:daeac9dd30cda8703c417e4fddccd7c4dc0c73421a0b54a7da2713be125846be", size = 222617, upload-time = "2025-04-10T22:19:23.773Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/db/3ed012b163e376fc461e1d6a67de69b408339bc31dc83d39ae9ec3bf9578/multidict-6.4.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f6f90700881438953eae443a9c6f8a509808bc3b185246992c4233ccee37fea", size = 227919, upload-time = "2025-04-10T22:19:25.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/db/0433c104bca380989bc04d3b841fc83e95ce0c89f680e9ea4251118b52b6/multidict-6.4.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f84627997008390dd15762128dcf73c3365f4ec0106739cde6c20a07ed198ec8", size = 226097, upload-time = "2025-04-10T22:19:27.183Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/95/910db2618175724dd254b7ae635b6cd8d2947a8b76b0376de7b96d814dab/multidict-6.4.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3307b48cd156153b117c0ea54890a3bdbf858a5b296ddd40dc3852e5f16e9b02", size = 220706, upload-time = "2025-04-10T22:19:28.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/af/aa176c6f5f1d901aac957d5258d5e22897fe13948d1e69063ae3d5d0ca01/multidict-6.4.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ead46b0fa1dcf5af503a46e9f1c2e80b5d95c6011526352fa5f42ea201526124", size = 211728, upload-time = "2025-04-10T22:19:30.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/42/d51cc5fc1527c3717d7f85137d6c79bb7a93cd214c26f1fc57523774dbb5/multidict-6.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1748cb2743bedc339d63eb1bca314061568793acd603a6e37b09a326334c9f44", size = 226276, upload-time = "2025-04-10T22:19:32.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/6b/d836dea45e0b8432343ba4acf9a8ecaa245da4c0960fb7ab45088a5e568a/multidict-6.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:acc9fa606f76fc111b4569348cc23a771cb52c61516dcc6bcef46d612edb483b", size = 212069, upload-time = "2025-04-10T22:19:34.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/34/0ee1a7adb3560e18ee9289c6e5f7db54edc312b13e5c8263e88ea373d12c/multidict-6.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:31469d5832b5885adeb70982e531ce86f8c992334edd2f2254a10fa3182ac504", size = 217858, upload-time = "2025-04-10T22:19:35.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/08/586d652c2f5acefe0cf4e658eedb4d71d4ba6dfd4f189bd81b400fc1bc6b/multidict-6.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ba46b51b6e51b4ef7bfb84b82f5db0dc5e300fb222a8a13b8cd4111898a869cf", size = 226988, upload-time = "2025-04-10T22:19:37.434Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/e3/cc59c7e2bc49d7f906fb4ffb6d9c3a3cf21b9f2dd9c96d05bef89c2b1fd1/multidict-6.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:389cfefb599edf3fcfd5f64c0410da686f90f5f5e2c4d84e14f6797a5a337af4", size = 220435, upload-time = "2025-04-10T22:19:39.005Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/32/5c3a556118aca9981d883f38c4b1bfae646f3627157f70f4068e5a648955/multidict-6.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:64bc2bbc5fba7b9db5c2c8d750824f41c6994e3882e6d73c903c2afa78d091e4", size = 221494, upload-time = "2025-04-10T22:19:41.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/3b/1599631f59024b75c4d6e3069f4502409970a336647502aaf6b62fb7ac98/multidict-6.4.3-cp313-cp313t-win32.whl", hash = "sha256:0ecdc12ea44bab2807d6b4a7e5eef25109ab1c82a8240d86d3c1fc9f3b72efd5", size = 41775, upload-time = "2025-04-10T22:19:43.707Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/4e/09301668d675d02ca8e8e1a3e6be046619e30403f5ada2ed5b080ae28d02/multidict-6.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7146a8742ea71b5d7d955bffcef58a9e6e04efba704b52a460134fefd10a8208", size = 45946, upload-time = "2025-04-10T22:19:45.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/10/7d526c8974f017f1e7ca584c71ee62a638e9334d8d33f27d7cdfc9ae79e4/multidict-6.4.3-py3-none-any.whl", hash = "sha256:59fe01ee8e2a1e8ceb3f6dbb216b09c8d9f4ef1c22c4fc825d045a147fa2ebc9", size = 10400, upload-time = "2025-04-10T22:20:16.445Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nbformat"
|
||||
version = "5.10.4"
|
||||
@@ -2455,63 +2273,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "propcache"
|
||||
version = "0.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/07/c8/fdc6686a986feae3541ea23dcaa661bd93972d3940460646c6bb96e21c40/propcache-0.3.1.tar.gz", hash = "sha256:40d980c33765359098837527e18eddefc9a24cea5b45e078a7f3bb5b032c6ecf", size = 43651, upload-time = "2025-03-26T03:06:12.05Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/41/aa/ca78d9be314d1e15ff517b992bebbed3bdfef5b8919e85bf4940e57b6137/propcache-0.3.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f78eb8422acc93d7b69964012ad7048764bb45a54ba7a39bb9e146c72ea29723", size = 80430, upload-time = "2025-03-26T03:04:26.436Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/d8/f0c17c44d1cda0ad1979af2e593ea290defdde9eaeb89b08abbe02a5e8e1/propcache-0.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:89498dd49c2f9a026ee057965cdf8192e5ae070ce7d7a7bd4b66a8e257d0c976", size = 46637, upload-time = "2025-03-26T03:04:27.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/bd/c1e37265910752e6e5e8a4c1605d0129e5b7933c3dc3cf1b9b48ed83b364/propcache-0.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09400e98545c998d57d10035ff623266927cb784d13dd2b31fd33b8a5316b85b", size = 46123, upload-time = "2025-03-26T03:04:30.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/b0/911eda0865f90c0c7e9f0415d40a5bf681204da5fd7ca089361a64c16b28/propcache-0.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8efd8c5adc5a2c9d3b952815ff8f7710cefdcaf5f2c36d26aff51aeca2f12f", size = 243031, upload-time = "2025-03-26T03:04:31.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/06/0da53397c76a74271621807265b6eb61fb011451b1ddebf43213df763669/propcache-0.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2fe5c910f6007e716a06d269608d307b4f36e7babee5f36533722660e8c4a70", size = 249100, upload-time = "2025-03-26T03:04:33.45Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/eb/13090e05bf6b963fc1653cdc922133ced467cb4b8dab53158db5a37aa21e/propcache-0.3.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0ab8cf8cdd2194f8ff979a43ab43049b1df0b37aa64ab7eca04ac14429baeb7", size = 250170, upload-time = "2025-03-26T03:04:35.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/4c/f72c9e1022b3b043ec7dc475a0f405d4c3e10b9b1d378a7330fecf0652da/propcache-0.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:563f9d8c03ad645597b8d010ef4e9eab359faeb11a0a2ac9f7b4bc8c28ebef25", size = 245000, upload-time = "2025-03-26T03:04:37.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/fd/970ca0e22acc829f1adf5de3724085e778c1ad8a75bec010049502cb3a86/propcache-0.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb6e0faf8cb6b4beea5d6ed7b5a578254c6d7df54c36ccd3d8b3eb00d6770277", size = 230262, upload-time = "2025-03-26T03:04:39.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/42/817289120c6b9194a44f6c3e6b2c3277c5b70bbad39e7df648f177cc3634/propcache-0.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1c5c7ab7f2bb3f573d1cb921993006ba2d39e8621019dffb1c5bc94cdbae81e8", size = 236772, upload-time = "2025-03-26T03:04:41.109Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9c/3b3942b302badd589ad6b672da3ca7b660a6c2f505cafd058133ddc73918/propcache-0.3.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:050b571b2e96ec942898f8eb46ea4bfbb19bd5502424747e83badc2d4a99a44e", size = 231133, upload-time = "2025-03-26T03:04:42.544Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/a1/75f6355f9ad039108ff000dfc2e19962c8dea0430da9a1428e7975cf24b2/propcache-0.3.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e1c4d24b804b3a87e9350f79e2371a705a188d292fd310e663483af6ee6718ee", size = 230741, upload-time = "2025-03-26T03:04:44.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/0c/3e82563af77d1f8731132166da69fdfd95e71210e31f18edce08a1eb11ea/propcache-0.3.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e4fe2a6d5ce975c117a6bb1e8ccda772d1e7029c1cca1acd209f91d30fa72815", size = 244047, upload-time = "2025-03-26T03:04:45.983Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f7/50/9fb7cca01532a08c4d5186d7bb2da6c4c587825c0ae134b89b47c7d62628/propcache-0.3.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:feccd282de1f6322f56f6845bf1207a537227812f0a9bf5571df52bb418d79d5", size = 246467, upload-time = "2025-03-26T03:04:47.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/02/ccbcf3e1c604c16cc525309161d57412c23cf2351523aedbb280eb7c9094/propcache-0.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec314cde7314d2dd0510c6787326bbffcbdc317ecee6b7401ce218b3099075a7", size = 241022, upload-time = "2025-03-26T03:04:49.195Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/19/e777227545e09ca1e77a6e21274ae9ec45de0f589f0ce3eca2a41f366220/propcache-0.3.1-cp312-cp312-win32.whl", hash = "sha256:7d2d5a0028d920738372630870e7d9644ce437142197f8c827194fca404bf03b", size = 40647, upload-time = "2025-03-26T03:04:50.595Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/bb/3b1b01da5dd04c77a204c84e538ff11f624e31431cfde7201d9110b092b1/propcache-0.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:88c423efef9d7a59dae0614eaed718449c09a5ac79a5f224a8b9664d603f04a3", size = 44784, upload-time = "2025-03-26T03:04:51.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/60/f645cc8b570f99be3cf46714170c2de4b4c9d6b827b912811eff1eb8a412/propcache-0.3.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f1528ec4374617a7a753f90f20e2f551121bb558fcb35926f99e3c42367164b8", size = 77865, upload-time = "2025-03-26T03:04:53.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/d4/c1adbf3901537582e65cf90fd9c26fde1298fde5a2c593f987112c0d0798/propcache-0.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc1915ec523b3b494933b5424980831b636fe483d7d543f7afb7b3bf00f0c10f", size = 45452, upload-time = "2025-03-26T03:04:54.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b5/fe752b2e63f49f727c6c1c224175d21b7d1727ce1d4873ef1c24c9216830/propcache-0.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a110205022d077da24e60b3df8bcee73971be9575dec5573dd17ae5d81751111", size = 44800, upload-time = "2025-03-26T03:04:55.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/37/fc357e345bc1971e21f76597028b059c3d795c5ca7690d7a8d9a03c9708a/propcache-0.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d249609e547c04d190e820d0d4c8ca03ed4582bcf8e4e160a6969ddfb57b62e5", size = 225804, upload-time = "2025-03-26T03:04:57.158Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/f1/16e12c33e3dbe7f8b737809bad05719cff1dccb8df4dafbcff5575002c0e/propcache-0.3.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ced33d827625d0a589e831126ccb4f5c29dfdf6766cac441d23995a65825dcb", size = 230650, upload-time = "2025-03-26T03:04:58.61Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/a2/018b9f2ed876bf5091e60153f727e8f9073d97573f790ff7cdf6bc1d1fb8/propcache-0.3.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4114c4ada8f3181af20808bedb250da6bae56660e4b8dfd9cd95d4549c0962f7", size = 234235, upload-time = "2025-03-26T03:05:00.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/5f/3faee66fc930dfb5da509e34c6ac7128870631c0e3582987fad161fcb4b1/propcache-0.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:975af16f406ce48f1333ec5e912fe11064605d5c5b3f6746969077cc3adeb120", size = 228249, upload-time = "2025-03-26T03:05:02.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/1e/a0d5ebda5da7ff34d2f5259a3e171a94be83c41eb1e7cd21a2105a84a02e/propcache-0.3.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a34aa3a1abc50740be6ac0ab9d594e274f59960d3ad253cd318af76b996dd654", size = 214964, upload-time = "2025-03-26T03:05:03.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/a0/d72da3f61ceab126e9be1f3bc7844b4e98c6e61c985097474668e7e52152/propcache-0.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9cec3239c85ed15bfaded997773fdad9fb5662b0a7cbc854a43f291eb183179e", size = 222501, upload-time = "2025-03-26T03:05:05.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/6d/a008e07ad7b905011253adbbd97e5b5375c33f0b961355ca0a30377504ac/propcache-0.3.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:05543250deac8e61084234d5fc54f8ebd254e8f2b39a16b1dce48904f45b744b", size = 217917, upload-time = "2025-03-26T03:05:06.59Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/37/02c9343ffe59e590e0e56dc5c97d0da2b8b19fa747ebacf158310f97a79a/propcache-0.3.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5cb5918253912e088edbf023788de539219718d3b10aef334476b62d2b53de53", size = 217089, upload-time = "2025-03-26T03:05:08.1Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/1b/d3406629a2c8a5666d4674c50f757a77be119b113eedd47b0375afdf1b42/propcache-0.3.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f3bbecd2f34d0e6d3c543fdb3b15d6b60dd69970c2b4c822379e5ec8f6f621d5", size = 228102, upload-time = "2025-03-26T03:05:09.982Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/a7/3664756cf50ce739e5f3abd48febc0be1a713b1f389a502ca819791a6b69/propcache-0.3.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aca63103895c7d960a5b9b044a83f544b233c95e0dcff114389d64d762017af7", size = 230122, upload-time = "2025-03-26T03:05:11.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/36/0bbabaacdcc26dac4f8139625e930f4311864251276033a52fd52ff2a274/propcache-0.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a0a9898fdb99bf11786265468571e628ba60af80dc3f6eb89a3545540c6b0ef", size = 226818, upload-time = "2025-03-26T03:05:12.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/27/4e0ef21084b53bd35d4dae1634b6d0bad35e9c58ed4f032511acca9d4d26/propcache-0.3.1-cp313-cp313-win32.whl", hash = "sha256:3a02a28095b5e63128bcae98eb59025924f121f048a62393db682f049bf4ac24", size = 40112, upload-time = "2025-03-26T03:05:14.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/2c/a54614d61895ba6dd7ac8f107e2b2a0347259ab29cbf2ecc7b94fa38c4dc/propcache-0.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:813fbb8b6aea2fc9659815e585e548fe706d6f663fa73dff59a1677d4595a037", size = 44034, upload-time = "2025-03-26T03:05:15.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/a8/0a4fd2f664fc6acc66438370905124ce62e84e2e860f2557015ee4a61c7e/propcache-0.3.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a444192f20f5ce8a5e52761a031b90f5ea6288b1eef42ad4c7e64fef33540b8f", size = 82613, upload-time = "2025-03-26T03:05:16.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/e5/5ef30eb2cd81576256d7b6caaa0ce33cd1d2c2c92c8903cccb1af1a4ff2f/propcache-0.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fbe94666e62ebe36cd652f5fc012abfbc2342de99b523f8267a678e4dfdee3c", size = 47763, upload-time = "2025-03-26T03:05:18.607Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/9a/87091ceb048efeba4d28e903c0b15bcc84b7c0bf27dc0261e62335d9b7b8/propcache-0.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f011f104db880f4e2166bcdcf7f58250f7a465bc6b068dc84c824a3d4a5c94dc", size = 47175, upload-time = "2025-03-26T03:05:19.85Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/2f/854e653c96ad1161f96194c6678a41bbb38c7947d17768e8811a77635a08/propcache-0.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e584b6d388aeb0001d6d5c2bd86b26304adde6d9bb9bfa9c4889805021b96de", size = 292265, upload-time = "2025-03-26T03:05:21.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/8d/090955e13ed06bc3496ba4a9fb26c62e209ac41973cb0d6222de20c6868f/propcache-0.3.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a17583515a04358b034e241f952f1715243482fc2c2945fd99a1b03a0bd77d6", size = 294412, upload-time = "2025-03-26T03:05:23.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/e6/d51601342e53cc7582449e6a3c14a0479fab2f0750c1f4d22302e34219c6/propcache-0.3.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5aed8d8308215089c0734a2af4f2e95eeb360660184ad3912686c181e500b2e7", size = 294290, upload-time = "2025-03-26T03:05:24.577Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/4d/be5f1a90abc1881884aa5878989a1acdafd379a91d9c7e5e12cef37ec0d7/propcache-0.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d8e309ff9a0503ef70dc9a0ebd3e69cf7b3894c9ae2ae81fc10943c37762458", size = 282926, upload-time = "2025-03-26T03:05:26.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/2b/8f61b998c7ea93a2b7eca79e53f3e903db1787fca9373af9e2cf8dc22f9d/propcache-0.3.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b655032b202028a582d27aeedc2e813299f82cb232f969f87a4fde491a233f11", size = 267808, upload-time = "2025-03-26T03:05:28.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/1c/311326c3dfce59c58a6098388ba984b0e5fb0381ef2279ec458ef99bd547/propcache-0.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f64d91b751df77931336b5ff7bafbe8845c5770b06630e27acd5dbb71e1931c", size = 290916, upload-time = "2025-03-26T03:05:29.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/74/91939924b0385e54dc48eb2e4edd1e4903ffd053cf1916ebc5347ac227f7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:19a06db789a4bd896ee91ebc50d059e23b3639c25d58eb35be3ca1cbe967c3bf", size = 262661, upload-time = "2025-03-26T03:05:31.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/d7/e6079af45136ad325c5337f5dd9ef97ab5dc349e0ff362fe5c5db95e2454/propcache-0.3.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:bef100c88d8692864651b5f98e871fb090bd65c8a41a1cb0ff2322db39c96c27", size = 264384, upload-time = "2025-03-26T03:05:32.984Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/d5/ba91702207ac61ae6f1c2da81c5d0d6bf6ce89e08a2b4d44e411c0bbe867/propcache-0.3.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:87380fb1f3089d2a0b8b00f006ed12bd41bd858fabfa7330c954c70f50ed8757", size = 291420, upload-time = "2025-03-26T03:05:34.496Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/70/2117780ed7edcd7ba6b8134cb7802aada90b894a9810ec56b7bb6018bee7/propcache-0.3.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e474fc718e73ba5ec5180358aa07f6aded0ff5f2abe700e3115c37d75c947e18", size = 290880, upload-time = "2025-03-26T03:05:36.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/1f/ecd9ce27710021ae623631c0146719280a929d895a095f6d85efb6a0be2e/propcache-0.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:17d1c688a443355234f3c031349da69444be052613483f3e4158eef751abcd8a", size = 287407, upload-time = "2025-03-26T03:05:37.799Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/66/2e90547d6b60180fb29e23dc87bd8c116517d4255240ec6d3f7dc23d1926/propcache-0.3.1-cp313-cp313t-win32.whl", hash = "sha256:359e81a949a7619802eb601d66d37072b79b79c2505e6d3fd8b945538411400d", size = 42573, upload-time = "2025-03-26T03:05:39.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/8f/50ad8599399d1861b4d2b6b45271f0ef6af1b09b0a2386a46dbaf19c9535/propcache-0.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e7fb9a84c9abbf2b2683fa3e7b0d7da4d8ecf139a1c635732a8bda29c5214b0e", size = 46757, upload-time = "2025-03-26T03:05:40.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/d3/c3cb8f1d6ae3b37f83e1de806713a9b3642c5895f0215a62e1a4bd6e5e34/propcache-0.3.1-py3-none-any.whl", hash = "sha256:9a8ecf38de50a7f518c21568c80f985e776397b902f1ce0b01f799aba1608b40", size = 12376, upload-time = "2025-03-26T03:06:10.5Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protobuf"
|
||||
version = "5.29.4"
|
||||
@@ -3509,30 +3270,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiktoken"
|
||||
version = "0.9.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "regex" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ea/cf/756fedf6981e82897f2d570dd25fa597eb3f4459068ae0572d7e888cfd6f/tiktoken-0.9.0.tar.gz", hash = "sha256:d02a5ca6a938e0490e1ff957bc48c8b078c88cb83977be1625b1fd8aac792c5d", size = 35991, upload-time = "2025-02-14T06:03:01.003Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cf/e5/21ff33ecfa2101c1bb0f9b6df750553bd873b7fb532ce2cb276ff40b197f/tiktoken-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e88f121c1c22b726649ce67c089b90ddda8b9662545a8aeb03cfef15967ddd03", size = 1065073, upload-time = "2025-02-14T06:02:24.768Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/03/a95e7b4863ee9ceec1c55983e4cc9558bcfd8f4f80e19c4f8a99642f697d/tiktoken-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6600660f2f72369acb13a57fb3e212434ed38b045fd8cc6cdd74947b4b5d210", size = 1008075, upload-time = "2025-02-14T06:02:26.92Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/10/1305bb02a561595088235a513ec73e50b32e74364fef4de519da69bc8010/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95e811743b5dfa74f4b227927ed86cbc57cad4df859cb3b643be797914e41794", size = 1140754, upload-time = "2025-02-14T06:02:28.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1b/40/da42522018ca496432ffd02793c3a72a739ac04c3794a4914570c9bb2925/tiktoken-0.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99376e1370d59bcf6935c933cb9ba64adc29033b7e73f5f7569f3aad86552b22", size = 1196678, upload-time = "2025-02-14T06:02:29.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/41/1e59dddaae270ba20187ceb8aa52c75b24ffc09f547233991d5fd822838b/tiktoken-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:badb947c32739fb6ddde173e14885fb3de4d32ab9d8c591cbd013c22b4c31dd2", size = 1259283, upload-time = "2025-02-14T06:02:33.838Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/11/09d936d37f49f4f494ffe660af44acd2d99eb2429d60a57c71318af214e0/tiktoken-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b0e8e05a26eda1249e824156d537015480af7ae222ccb798e5234ae0285dbdb", size = 1064919, upload-time = "2025-02-14T06:02:37.494Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/0e/f38ba35713edb8d4197ae602e80837d574244ced7fb1b6070b31c29816e0/tiktoken-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:27d457f096f87685195eea0165a1807fae87b97b2161fe8c9b1df5bd74ca6f63", size = 1007877, upload-time = "2025-02-14T06:02:39.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/82/9197f77421e2a01373e27a79dd36efdd99e6b4115746ecc553318ecafbf0/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cf8ded49cddf825390e36dd1ad35cd49589e8161fdcb52aa25f0583e90a3e01", size = 1140095, upload-time = "2025-02-14T06:02:41.791Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/bb/4513da71cac187383541facd0291c4572b03ec23c561de5811781bbd988f/tiktoken-0.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc156cb314119a8bb9748257a2eaebd5cc0753b6cb491d26694ed42fc7cb3139", size = 1195649, upload-time = "2025-02-14T06:02:43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/5c/74e4c137530dd8504e97e3a41729b1103a4ac29036cbfd3250b11fd29451/tiktoken-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cd69372e8c9dd761f0ab873112aba55a0e3e506332dd9f7522ca466e817b1b7a", size = 1258465, upload-time = "2025-02-14T06:02:45.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/a8/8f499c179ec900783ffe133e9aab10044481679bb9aad78436d239eee716/tiktoken-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:5ea0edb6f83dc56d794723286215918c1cde03712cbbafa0348b33448faf5b95", size = 894669, upload-time = "2025-02-14T06:02:47.341Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokenizers"
|
||||
version = "0.22.2"
|
||||
@@ -3695,11 +3432,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.13.2"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3909,71 +3646,6 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/03/15cd49e855c62226ecf1831bbe4c8e73a4324856077a23c495538a36e557/xgboost-3.0.0-py3-none-win_amd64.whl", hash = "sha256:0ea74e97f95b1eddfd27a46b7f22f72ec5a5322e1dc7cb41c9c23fb580763df9", size = 149971978, upload-time = "2025-03-15T13:53:26.596Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "multidict" },
|
||||
{ name = "propcache" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/62/51/c0edba5219027f6eab262e139f73e2417b0f4efffa23bf562f6e18f76ca5/yarl-1.20.0.tar.gz", hash = "sha256:686d51e51ee5dfe62dec86e4866ee0e9ed66df700d55c828a615640adc885307", size = 185258, upload-time = "2025-04-17T00:45:14.661Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/e8/3efdcb83073df978bb5b1a9cc0360ce596680e6c3fac01f2a994ccbb8939/yarl-1.20.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e06b9f6cdd772f9b665e5ba8161968e11e403774114420737f7884b5bd7bdf6f", size = 147089, upload-time = "2025-04-17T00:42:39.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/c3/9e776e98ea350f76f94dd80b408eaa54e5092643dbf65fd9babcffb60509/yarl-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b9ae2fbe54d859b3ade40290f60fe40e7f969d83d482e84d2c31b9bff03e359e", size = 97706, upload-time = "2025-04-17T00:42:41.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/5b/45cdfb64a3b855ce074ae607b9fc40bc82e7613b94e7612b030255c93a09/yarl-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d12b8945250d80c67688602c891237994d203d42427cb14e36d1a732eda480e", size = 95719, upload-time = "2025-04-17T00:42:43.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/4e/929633b249611eeed04e2f861a14ed001acca3ef9ec2a984a757b1515889/yarl-1.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:087e9731884621b162a3e06dc0d2d626e1542a617f65ba7cc7aeab279d55ad33", size = 343972, upload-time = "2025-04-17T00:42:45.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/fd/047535d326c913f1a90407a3baf7ff535b10098611eaef2c527e32e81ca1/yarl-1.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69df35468b66c1a6e6556248e6443ef0ec5f11a7a4428cf1f6281f1879220f58", size = 339639, upload-time = "2025-04-17T00:42:47.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/2f/11566f1176a78f4bafb0937c0072410b1b0d3640b297944a6a7a556e1d0b/yarl-1.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b2992fe29002fd0d4cbaea9428b09af9b8686a9024c840b8a2b8f4ea4abc16f", size = 353745, upload-time = "2025-04-17T00:42:49.406Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/17/07dfcf034d6ae8837b33988be66045dd52f878dfb1c4e8f80a7343f677be/yarl-1.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c903e0b42aab48abfbac668b5a9d7b6938e721a6341751331bcd7553de2dcae", size = 354178, upload-time = "2025-04-17T00:42:51.588Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/45/212604d3142d84b4065d5f8cab6582ed3d78e4cc250568ef2a36fe1cf0a5/yarl-1.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf099e2432131093cc611623e0b0bcc399b8cddd9a91eded8bfb50402ec35018", size = 349219, upload-time = "2025-04-17T00:42:53.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/e0/a10b30f294111c5f1c682461e9459935c17d467a760c21e1f7db400ff499/yarl-1.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7f62f5dc70a6c763bec9ebf922be52aa22863d9496a9a30124d65b489ea672", size = 337266, upload-time = "2025-04-17T00:42:55.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/33/a6/6efa1d85a675d25a46a167f9f3e80104cde317dfdf7f53f112ae6b16a60a/yarl-1.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:54ac15a8b60382b2bcefd9a289ee26dc0920cf59b05368c9b2b72450751c6eb8", size = 360873, upload-time = "2025-04-17T00:42:57.895Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/67/c8ab718cb98dfa2ae9ba0f97bf3cbb7d45d37f13fe1fbad25ac92940954e/yarl-1.20.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:25b3bc0763a7aca16a0f1b5e8ef0f23829df11fb539a1b70476dcab28bd83da7", size = 360524, upload-time = "2025-04-17T00:43:00.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/e8/c3f18660cea1bc73d9f8a2b3ef423def8dadbbae6c4afabdb920b73e0ead/yarl-1.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b2586e36dc070fc8fad6270f93242124df68b379c3a251af534030a4a33ef594", size = 365370, upload-time = "2025-04-17T00:43:02.242Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/99/33f3b97b065e62ff2d52817155a89cfa030a1a9b43fee7843ef560ad9603/yarl-1.20.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:866349da9d8c5290cfefb7fcc47721e94de3f315433613e01b435473be63daa6", size = 373297, upload-time = "2025-04-17T00:43:04.189Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/89/7519e79e264a5f08653d2446b26d4724b01198a93a74d2e259291d538ab1/yarl-1.20.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:33bb660b390a0554d41f8ebec5cd4475502d84104b27e9b42f5321c5192bfcd1", size = 378771, upload-time = "2025-04-17T00:43:06.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/58/6c460bbb884abd2917c3eef6f663a4a873f8dc6f498561fc0ad92231c113/yarl-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737e9f171e5a07031cbee5e9180f6ce21a6c599b9d4b2c24d35df20a52fabf4b", size = 375000, upload-time = "2025-04-17T00:43:09.01Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/2a/dd7ed1aa23fea996834278d7ff178f215b24324ee527df53d45e34d21d28/yarl-1.20.0-cp312-cp312-win32.whl", hash = "sha256:839de4c574169b6598d47ad61534e6981979ca2c820ccb77bf70f4311dd2cc64", size = 86355, upload-time = "2025-04-17T00:43:11.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/c6/333fe0338305c0ac1c16d5aa7cc4841208d3252bbe62172e0051006b5445/yarl-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:3d7dbbe44b443b0c4aa0971cb07dcb2c2060e4a9bf8d1301140a33a93c98e18c", size = 92904, upload-time = "2025-04-17T00:43:13.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/6f/514c9bff2900c22a4f10e06297714dbaf98707143b37ff0bcba65a956221/yarl-1.20.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2137810a20b933b1b1b7e5cf06a64c3ed3b4747b0e5d79c9447c00db0e2f752f", size = 145030, upload-time = "2025-04-17T00:43:15.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/9d/f88da3fa319b8c9c813389bfb3463e8d777c62654c7168e580a13fadff05/yarl-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:447c5eadd750db8389804030d15f43d30435ed47af1313303ed82a62388176d3", size = 96894, upload-time = "2025-04-17T00:43:17.372Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/57/92e83538580a6968b2451d6c89c5579938a7309d4785748e8ad42ddafdce/yarl-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42fbe577272c203528d402eec8bf4b2d14fd49ecfec92272334270b850e9cd7d", size = 94457, upload-time = "2025-04-17T00:43:19.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ee/7ee43bd4cf82dddd5da97fcaddb6fa541ab81f3ed564c42f146c83ae17ce/yarl-1.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18e321617de4ab170226cd15006a565d0fa0d908f11f724a2c9142d6b2812ab0", size = 343070, upload-time = "2025-04-17T00:43:21.426Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/12/b5eccd1109e2097bcc494ba7dc5de156e41cf8309fab437ebb7c2b296ce3/yarl-1.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4345f58719825bba29895011e8e3b545e6e00257abb984f9f27fe923afca2501", size = 337739, upload-time = "2025-04-17T00:43:23.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/6b/0eade8e49af9fc2585552f63c76fa59ef469c724cc05b29519b19aa3a6d5/yarl-1.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d9b980d7234614bc4674468ab173ed77d678349c860c3af83b1fffb6a837ddc", size = 351338, upload-time = "2025-04-17T00:43:25.695Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/cb/aaaa75d30087b5183c7b8a07b4fb16ae0682dd149a1719b3a28f54061754/yarl-1.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af4baa8a445977831cbaa91a9a84cc09debb10bc8391f128da2f7bd070fc351d", size = 353636, upload-time = "2025-04-17T00:43:27.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/9d/d9cb39ec68a91ba6e66fa86d97003f58570327d6713833edf7ad6ce9dde5/yarl-1.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:123393db7420e71d6ce40d24885a9e65eb1edefc7a5228db2d62bcab3386a5c0", size = 348061, upload-time = "2025-04-17T00:43:29.788Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/6b/103940aae893d0cc770b4c36ce80e2ed86fcb863d48ea80a752b8bda9303/yarl-1.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab47acc9332f3de1b39e9b702d9c916af7f02656b2a86a474d9db4e53ef8fd7a", size = 334150, upload-time = "2025-04-17T00:43:31.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/b2/986bd82aa222c3e6b211a69c9081ba46484cffa9fab2a5235e8d18ca7a27/yarl-1.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4a34c52ed158f89876cba9c600b2c964dfc1ca52ba7b3ab6deb722d1d8be6df2", size = 362207, upload-time = "2025-04-17T00:43:34.099Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/7c/63f5922437b873795d9422cbe7eb2509d4b540c37ae5548a4bb68fd2c546/yarl-1.20.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:04d8cfb12714158abf2618f792c77bc5c3d8c5f37353e79509608be4f18705c9", size = 361277, upload-time = "2025-04-17T00:43:36.202Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/83/450938cccf732466953406570bdb42c62b5ffb0ac7ac75a1f267773ab5c8/yarl-1.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7dc63ad0d541c38b6ae2255aaa794434293964677d5c1ec5d0116b0e308031f5", size = 364990, upload-time = "2025-04-17T00:43:38.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/de/af47d3a47e4a833693b9ec8e87debb20f09d9fdc9139b207b09a3e6cbd5a/yarl-1.20.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f9d02b591a64e4e6ca18c5e3d925f11b559c763b950184a64cf47d74d7e41877", size = 374684, upload-time = "2025-04-17T00:43:40.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/0b/078bcc2d539f1faffdc7d32cb29a2d7caa65f1a6f7e40795d8485db21851/yarl-1.20.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:95fc9876f917cac7f757df80a5dda9de59d423568460fe75d128c813b9af558e", size = 382599, upload-time = "2025-04-17T00:43:42.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/a9/4fdb1a7899f1fb47fd1371e7ba9e94bff73439ce87099d5dd26d285fffe0/yarl-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bb769ae5760cd1c6a712135ee7915f9d43f11d9ef769cb3f75a23e398a92d384", size = 378573, upload-time = "2025-04-17T00:43:44.797Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/be/29f5156b7a319e4d2e5b51ce622b4dfb3aa8d8204cd2a8a339340fbfad40/yarl-1.20.0-cp313-cp313-win32.whl", hash = "sha256:70e0c580a0292c7414a1cead1e076c9786f685c1fc4757573d2967689b370e62", size = 86051, upload-time = "2025-04-17T00:43:47.076Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/56/05fa52c32c301da77ec0b5f63d2d9605946fe29defacb2a7ebd473c23b81/yarl-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:4c43030e4b0af775a85be1fa0433119b1565673266a70bf87ef68a9d5ba3174c", size = 92742, upload-time = "2025-04-17T00:43:49.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/2f/422546794196519152fc2e2f475f0e1d4d094a11995c81a465faf5673ffd/yarl-1.20.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b6c4c3d0d6a0ae9b281e492b1465c72de433b782e6b5001c8e7249e085b69051", size = 163575, upload-time = "2025-04-17T00:43:51.533Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/fc/67c64ddab6c0b4a169d03c637fb2d2a212b536e1989dec8e7e2c92211b7f/yarl-1.20.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8681700f4e4df891eafa4f69a439a6e7d480d64e52bf460918f58e443bd3da7d", size = 106121, upload-time = "2025-04-17T00:43:53.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/00/29366b9eba7b6f6baed7d749f12add209b987c4cfbfa418404dbadc0f97c/yarl-1.20.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:84aeb556cb06c00652dbf87c17838eb6d92cfd317799a8092cee0e570ee11229", size = 103815, upload-time = "2025-04-17T00:43:55.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/f4/a2a4c967c8323c03689383dff73396281ced3b35d0ed140580825c826af7/yarl-1.20.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f166eafa78810ddb383e930d62e623d288fb04ec566d1b4790099ae0f31485f1", size = 408231, upload-time = "2025-04-17T00:43:57.825Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/a1/66f7ffc0915877d726b70cc7a896ac30b6ac5d1d2760613603b022173635/yarl-1.20.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5d3d6d14754aefc7a458261027a562f024d4f6b8a798adb472277f675857b1eb", size = 390221, upload-time = "2025-04-17T00:44:00.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/15/cc248f0504610283271615e85bf38bc014224122498c2016d13a3a1b8426/yarl-1.20.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a8f64df8ed5d04c51260dbae3cc82e5649834eebea9eadfd829837b8093eb00", size = 411400, upload-time = "2025-04-17T00:44:02.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/af/f0823d7e092bfb97d24fce6c7269d67fcd1aefade97d0a8189c4452e4d5e/yarl-1.20.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d9949eaf05b4d30e93e4034a7790634bbb41b8be2d07edd26754f2e38e491de", size = 411714, upload-time = "2025-04-17T00:44:04.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/70/be418329eae64b9f1b20ecdaac75d53aef098797d4c2299d82ae6f8e4663/yarl-1.20.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c366b254082d21cc4f08f522ac201d0d83a8b8447ab562732931d31d80eb2a5", size = 404279, upload-time = "2025-04-17T00:44:07.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/f5/52e02f0075f65b4914eb890eea1ba97e6fd91dd821cc33a623aa707b2f67/yarl-1.20.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91bc450c80a2e9685b10e34e41aef3d44ddf99b3a498717938926d05ca493f6a", size = 384044, upload-time = "2025-04-17T00:44:09.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/36/b0fa25226b03d3f769c68d46170b3e92b00ab3853d73127273ba22474697/yarl-1.20.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9c2aa4387de4bc3a5fe158080757748d16567119bef215bec643716b4fbf53f9", size = 416236, upload-time = "2025-04-17T00:44:11.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/3a/54c828dd35f6831dfdd5a79e6c6b4302ae2c5feca24232a83cb75132b205/yarl-1.20.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:d2cbca6760a541189cf87ee54ff891e1d9ea6406079c66341008f7ef6ab61145", size = 402034, upload-time = "2025-04-17T00:44:13.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/97/c7bf5fba488f7e049f9ad69c1b8fdfe3daa2e8916b3d321aa049e361a55a/yarl-1.20.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:798a5074e656f06b9fad1a162be5a32da45237ce19d07884d0b67a0aa9d5fdda", size = 407943, upload-time = "2025-04-17T00:44:16.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/a4/022d2555c1e8fcff08ad7f0f43e4df3aba34f135bff04dd35d5526ce54ab/yarl-1.20.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f106e75c454288472dbe615accef8248c686958c2e7dd3b8d8ee2669770d020f", size = 423058, upload-time = "2025-04-17T00:44:18.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/f6/0873a05563e5df29ccf35345a6ae0ac9e66588b41fdb7043a65848f03139/yarl-1.20.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3b60a86551669c23dc5445010534d2c5d8a4e012163218fc9114e857c0586fdd", size = 423792, upload-time = "2025-04-17T00:44:20.639Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/35/43fbbd082708fa42e923f314c24f8277a28483d219e049552e5007a9aaca/yarl-1.20.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e429857e341d5e8e15806118e0294f8073ba9c4580637e59ab7b238afca836f", size = 422242, upload-time = "2025-04-17T00:44:22.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/f7/f0f2500cf0c469beb2050b522c7815c575811627e6d3eb9ec7550ddd0bfe/yarl-1.20.0-cp313-cp313t-win32.whl", hash = "sha256:65a4053580fe88a63e8e4056b427224cd01edfb5f951498bfefca4052f0ce0ac", size = 93816, upload-time = "2025-04-17T00:44:25.491Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/93/f73b61353b2a699d489e782c3f5998b59f974ec3156a2050a52dfd7e8946/yarl-1.20.0-cp313-cp313t-win_amd64.whl", hash = "sha256:53b2da3a6ca0a541c1ae799c349788d480e5144cac47dba0266c7cb6c76151fe", size = 101093, upload-time = "2025-04-17T00:44:27.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/1f/70c57b3d7278e94ed22d85e09685d3f0a38ebdd8c5c73b65ba4c0d0fe002/yarl-1.20.0-py3-none-any.whl", hash = "sha256:5d0fe6af927a47a230f31e6004621fd0959eaa915fc62acfafa67ff7229a3124", size = 46124, upload-time = "2025-04-17T00:45:12.199Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.21.0"
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface ValidateApiKeyRequest {
|
||||
api_key: string;
|
||||
base_url?: string;
|
||||
model_id: string;
|
||||
api_type?: string;
|
||||
}
|
||||
|
||||
/** 验证 API Key 响应 */
|
||||
@@ -19,25 +20,25 @@ export interface SaveApiConfigRequest {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
apiType: string;
|
||||
};
|
||||
modeler: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
apiType: string;
|
||||
};
|
||||
coder: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
apiType: string;
|
||||
};
|
||||
writer: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
apiType: string;
|
||||
};
|
||||
openalex_email: string;
|
||||
}
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { ref, computed, onUnmounted } from "vue";
|
||||
|
||||
// ---- Props ----
|
||||
|
||||
/** 审批消息数据 */
|
||||
interface ApprovalData {
|
||||
checkpoint_id: string;
|
||||
prompt: Record<string, unknown>;
|
||||
options: string[];
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
// ---- State ----
|
||||
|
||||
const showDialog = ref(false);
|
||||
const approvalData = ref<ApprovalData | null>(null);
|
||||
const editContent = ref("");
|
||||
const showEditArea = ref(false);
|
||||
const showAskArea = ref(false);
|
||||
const askContent = ref("");
|
||||
const remainingSeconds = ref(0);
|
||||
|
||||
let resolvePromise: ((value: { action: string; content?: unknown }) => void) | null = null;
|
||||
let countdownTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
// ---- Computed ----
|
||||
|
||||
const promptDisplay = computed(() => {
|
||||
if (!approvalData.value) return "";
|
||||
const p = approvalData.value.prompt;
|
||||
if (p.step) return `步骤: ${p.step}`;
|
||||
if (p.subtask) return `子任务: ${p.subtask}`;
|
||||
return JSON.stringify(p, null, 2);
|
||||
});
|
||||
|
||||
const promptDetail = computed(() => {
|
||||
if (!approvalData.value) return "";
|
||||
const p = approvalData.value.prompt;
|
||||
// 显示关键内容摘要
|
||||
if (p.questions) return JSON.stringify(p.questions, null, 2).slice(0, 500);
|
||||
if (p.solutions) return JSON.stringify(p.solutions, null, 2).slice(0, 500);
|
||||
if (p.code_response) return String(p.code_response).slice(0, 500);
|
||||
if (p.summary) return String(p.summary).slice(0, 500);
|
||||
return "";
|
||||
});
|
||||
|
||||
// ---- Methods ----
|
||||
|
||||
/** 打开审批对话框 */
|
||||
function open(data: ApprovalData): Promise<{ action: string; content?: unknown }> {
|
||||
approvalData.value = data;
|
||||
editContent.value = "";
|
||||
askContent.value = "";
|
||||
showEditArea.value = false;
|
||||
showAskArea.value = false;
|
||||
showDialog.value = true;
|
||||
remainingSeconds.value = data.timeout;
|
||||
|
||||
// 启动倒计时
|
||||
if (countdownTimer) clearInterval(countdownTimer);
|
||||
countdownTimer = setInterval(() => {
|
||||
remainingSeconds.value = Math.max(0, remainingSeconds.value - 1);
|
||||
if (remainingSeconds.value <= 0) {
|
||||
handleAction("confirm");
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
resolvePromise = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
/** 处理用户决策 */
|
||||
function handleAction(action: string) {
|
||||
cleanup();
|
||||
showDialog.value = false;
|
||||
|
||||
const result: { action: string; content?: unknown } = { action };
|
||||
|
||||
if (action === "edit" && editContent.value) {
|
||||
try {
|
||||
result.content = JSON.parse(editContent.value);
|
||||
} catch {
|
||||
result.content = editContent.value;
|
||||
}
|
||||
} else if (action === "ask" && askContent.value) {
|
||||
result.content = askContent.value;
|
||||
}
|
||||
|
||||
resolvePromise?.(result);
|
||||
resolvePromise = null;
|
||||
}
|
||||
|
||||
/** 清理定时器 */
|
||||
function cleanup() {
|
||||
if (countdownTimer) {
|
||||
clearInterval(countdownTimer);
|
||||
countdownTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
onUnmounted(cleanup);
|
||||
|
||||
// ---- Expose ----
|
||||
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog v-model:open="showDialog">
|
||||
<DialogContent class="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>需要您的审批</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="space-y-4 py-2">
|
||||
<!-- 检查点信息 -->
|
||||
<div class="rounded-md bg-muted p-3">
|
||||
<p class="text-sm font-medium">{{ promptDisplay }}</p>
|
||||
<pre v-if="promptDetail" class="mt-2 max-h-40 overflow-auto text-xs text-muted-foreground whitespace-pre-wrap">{{ promptDetail }}</pre>
|
||||
</div>
|
||||
|
||||
<!-- 倒计时 -->
|
||||
<div class="text-sm text-muted-foreground text-center">
|
||||
<span v-if="remainingSeconds > 0">
|
||||
{{ remainingSeconds }}秒后自动继续
|
||||
</span>
|
||||
<span v-else class="text-orange-500">超时,自动继续...</span>
|
||||
</div>
|
||||
|
||||
<!-- 编辑区域 -->
|
||||
<div v-if="showEditArea" class="space-y-2">
|
||||
<p class="text-sm text-muted-foreground">请输入修改后的内容(JSON 或文本):</p>
|
||||
<Textarea v-model="editContent" rows="6" placeholder="输入修改后的内容..." />
|
||||
</div>
|
||||
|
||||
<!-- 追问区域 -->
|
||||
<div v-if="showAskArea" class="space-y-2">
|
||||
<p class="text-sm text-muted-foreground">请输入补充信息:</p>
|
||||
<Textarea v-model="askContent" rows="3" placeholder="输入补充信息..." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter class="flex flex-wrap gap-2">
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="default" @click="handleAction('confirm')">
|
||||
确认继续
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="outline" @click="showEditArea = true">
|
||||
修改内容
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="outline" @click="handleAction('regenerate')">
|
||||
重新生成
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="outline" @click="showAskArea = true">
|
||||
追问
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="ghost" @click="handleAction('skip')">
|
||||
跳过审核
|
||||
</Button>
|
||||
<Button v-if="!showEditArea && !showAskArea" variant="destructive" @click="handleAction('abort')">
|
||||
中止任务
|
||||
</Button>
|
||||
|
||||
<!-- 编辑/追问确认 -->
|
||||
<Button v-if="showEditArea" variant="default" @click="handleAction('edit')">
|
||||
提交修改
|
||||
</Button>
|
||||
<Button v-if="showEditArea" variant="ghost" @click="showEditArea = false">
|
||||
取消
|
||||
</Button>
|
||||
<Button v-if="showAskArea" variant="default" @click="handleAction('ask')">
|
||||
发送追问
|
||||
</Button>
|
||||
<Button v-if="showAskArea" variant="ghost" @click="showAskArea = false">
|
||||
取消
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
@@ -36,52 +36,57 @@ const emit = defineEmits<(e: "update:open", value: boolean) => void>();
|
||||
|
||||
const apiKeyStore = useApiKeyStore();
|
||||
|
||||
/** API 类型选项 */
|
||||
const apiTypeOptions = [
|
||||
{ value: "openai-chat", label: "OpenAI Chat" },
|
||||
{ value: "openai-responses", label: "OpenAI Responses" },
|
||||
{ value: "anthropic", label: "Anthropic" },
|
||||
];
|
||||
|
||||
/** Agent 表单配置 */
|
||||
interface AgentFormConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
apiType: string;
|
||||
contextWindow: number;
|
||||
}
|
||||
|
||||
/** 本地表单数据 */
|
||||
const form = ref<{
|
||||
coordinator: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
modeler: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
coder: { apiKey: string; baseUrl: string; modelId: string; provider: string };
|
||||
writer: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
};
|
||||
coordinator: AgentFormConfig;
|
||||
modeler: AgentFormConfig;
|
||||
coder: AgentFormConfig;
|
||||
writer: AgentFormConfig;
|
||||
openalex_email: string;
|
||||
}>({
|
||||
coordinator: {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
modeler: {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
coder: {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
writer: {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
openalex_email: "",
|
||||
});
|
||||
@@ -170,6 +175,7 @@ const validateModelApiKey = async (config: {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
apiType: string;
|
||||
}) => {
|
||||
if (!config.apiKey) {
|
||||
return { valid: false, message: "API Key 为空" };
|
||||
@@ -184,6 +190,7 @@ const validateModelApiKey = async (config: {
|
||||
api_key: config.apiKey,
|
||||
base_url: config.baseUrl || "https://api.openai.com/v1",
|
||||
model_id: config.modelId,
|
||||
api_type: config.apiType || "openai-chat",
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -221,6 +228,7 @@ const validateAllApiKeys = async () => {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
apiType: string;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -251,71 +259,37 @@ const validateAllApiKeys = async () => {
|
||||
/** 重置所有表单数据 */
|
||||
const resetAll = () => {
|
||||
form.value = {
|
||||
coordinator: { apiKey: "", baseUrl: "", modelId: "", provider: "" },
|
||||
modeler: { apiKey: "", baseUrl: "", modelId: "", provider: "" },
|
||||
coder: { apiKey: "", baseUrl: "", modelId: "", provider: "" },
|
||||
writer: { apiKey: "", baseUrl: "", modelId: "", provider: "" },
|
||||
coordinator: {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
modeler: {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
coder: {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
writer: {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
},
|
||||
openalex_email: "",
|
||||
};
|
||||
};
|
||||
|
||||
/** 预设的模型提供商配置 */
|
||||
const providers = {
|
||||
DeepSeek: {
|
||||
url: "https://platform.deepseek.com/api_keys",
|
||||
key: "DeepSeek",
|
||||
baseUrl: "https://api.deepseek.com",
|
||||
modelId: "deepseek/deepseek-chat",
|
||||
},
|
||||
硅基流动: {
|
||||
url: "https://cloud.siliconflow.cn/i/UIb4Enf4",
|
||||
key: "硅基流动",
|
||||
baseUrl: "https://api.siliconflow.cn",
|
||||
modelId: "openai/deepseek-ai/DeepSeek-V3",
|
||||
},
|
||||
Sophnet: {
|
||||
url: "https://www.sophnet.com/#?code=AZBSFG",
|
||||
key: "Sophnet",
|
||||
baseUrl: "https://www.sophnet.com/api/open-apis",
|
||||
modelId: "openai/DeepSeek-V3-Fast",
|
||||
},
|
||||
OpenAI: {
|
||||
url: "https://platform.openai.com/api-keys",
|
||||
key: "OpenAI",
|
||||
baseUrl: "https://api.openai.com",
|
||||
modelId: "openai/gpt-5",
|
||||
},
|
||||
"302.AI": {
|
||||
url: "https://share.302.ai/UoTruU",
|
||||
key: "302.AI",
|
||||
baseUrl: "https://api.302.ai",
|
||||
modelId: "openai/deepseek-chat",
|
||||
},
|
||||
"OpenAI 兼容": {
|
||||
url: "/",
|
||||
key: "OpenAI 兼容",
|
||||
baseUrl: "basurl",
|
||||
modelId: "provider/model_id",
|
||||
},
|
||||
};
|
||||
|
||||
/** 当供应商选择改变时,自动填写配置 */
|
||||
const onProviderChange = (configKey: string, providerKey: string) => {
|
||||
const provider = providers[providerKey as keyof typeof providers];
|
||||
if (provider) {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: 动态访问表单配置
|
||||
const formConfig = (form.value as any)[configKey];
|
||||
formConfig.provider = providerKey;
|
||||
formConfig.baseUrl = provider.baseUrl;
|
||||
formConfig.modelId = provider.modelId;
|
||||
|
||||
validationResults.value[configKey as keyof typeof validationResults.value] =
|
||||
{
|
||||
valid: false,
|
||||
message: "",
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -324,13 +298,7 @@ const onProviderChange = (configKey: string, providerKey: string) => {
|
||||
<DialogHeader>
|
||||
<DialogTitle>设置</DialogTitle>
|
||||
<DialogDescription>
|
||||
为每个 Agent 配置合适模型
|
||||
<br>
|
||||
<div><a href="https://docs.litellm.ai/docs/providers" target="_blank"
|
||||
class="text-blue-600 hover:text-blue-800 underline text-xs">
|
||||
more details
|
||||
</a>
|
||||
</div>
|
||||
为每个 Agent 配置 API 类型和模型
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -341,39 +309,27 @@ const onProviderChange = (configKey: string, providerKey: string) => {
|
||||
<h3 class="text-sm font-medium">{{ config.label }}</h3>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1">
|
||||
<Label :for="`${config.key}-provider`" class="text-xs text-muted-foreground">提供商</Label>
|
||||
|
||||
<div class="flex gap-2 items-center">
|
||||
<Select :model-value="(form as any)[config.key].provider"
|
||||
@update:model-value="(value: any) => value && onProviderChange(config.key, String(value))">
|
||||
<SelectTrigger class="w-[120px] h-7 text-xs">
|
||||
<SelectValue placeholder="选择提供商" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>提供商</SelectLabel>
|
||||
<SelectItem v-for="(provider, key) in providers" :key="key" :value="key">
|
||||
{{ provider.key }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div v-if="(form as any)[config.key].provider">
|
||||
<a :href="providers[(form as any)[config.key].provider as keyof typeof providers]?.url"
|
||||
target="_blank" class="text-blue-600 hover:text-blue-800 underline text-xs">
|
||||
{{ providers[(form as any)[config.key].provider as keyof typeof providers]?.key }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Label :for="`${config.key}-api-type`" class="text-xs text-muted-foreground">API 类型</Label>
|
||||
<Select :model-value="(form as any)[config.key].apiType"
|
||||
@update:model-value="(value: any) => { (form as any)[config.key].apiType = value }">
|
||||
<SelectTrigger class="w-full h-7 text-xs">
|
||||
<SelectValue placeholder="选择 API 类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>API 类型</SelectLabel>
|
||||
<SelectItem v-for="opt in apiTypeOptions" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</SelectItem>
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
|
||||
<Label :for="`${config.key}-api-key`" class="text-xs text-muted-foreground">API Key</Label>
|
||||
|
||||
<Input :id="`${config.key}-api-key`" v-model.trim="(form as any)[config.key].apiKey" type="password"
|
||||
placeholder="请输入 API Key" class="h-7 text-xs flex-1" />
|
||||
|
||||
<div v-if="validationResults[config.key as keyof typeof validationResults].message"
|
||||
class="flex items-center">
|
||||
<CheckCircle v-if="validationResults[config.key as keyof typeof validationResults].valid"
|
||||
@@ -381,21 +337,28 @@ const onProviderChange = (configKey: string, providerKey: string) => {
|
||||
<XCircle v-else class="h-4 w-4 text-red-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1">
|
||||
<Label :for="`${config.key}-base-url`" class="text-xs text-muted-foreground">Base URL</Label>
|
||||
<Input :id="`${config.key}-base-url`" v-model.trim="(form as any)[config.key].baseUrl"
|
||||
placeholder="baseUrl" class="h-7 text-xs" />
|
||||
placeholder="https://api.openai.com/v1" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label :for="`${config.key}-model-id`" class="text-xs text-muted-foreground">Model ID</Label>
|
||||
<Input :id="`${config.key}-model-id`" v-model.trim="(form as any)[config.key].modelId"
|
||||
placeholder="provider/model_id" class="h-7 text-xs" />
|
||||
placeholder="gpt-4o / claude-sonnet-4-20250514" class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label :for="`${config.key}-context-window`" class="text-xs text-muted-foreground">
|
||||
上下文窗口(token)
|
||||
</Label>
|
||||
<Input :id="`${config.key}-context-window`"
|
||||
v-model.number="(form as any)[config.key].contextWindow" type="number"
|
||||
placeholder="128000" class="h-7 text-xs" min="4096" step="1024" />
|
||||
</div>
|
||||
<div v-if="validationResults[config.key as keyof typeof validationResults].message" :class="[
|
||||
'text-xs px-2 py-1 rounded text-left border',
|
||||
validationResults[config.key as keyof typeof validationResults].valid ? 'bg-green-50 text-green-700 border-green-200' : 'bg-red-50 text-red-700 border-red-200'
|
||||
@@ -405,8 +368,6 @@ const onProviderChange = (configKey: string, providerKey: string) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-sm font-medium">其他</h3>
|
||||
<Label :for="`openalex-email`" class="text-xs text-muted-foreground">OpenAlex Email</Label>
|
||||
|
||||
@@ -95,6 +95,24 @@ onBeforeUnmount(() => {
|
||||
<div class="text-sm text-gray-600">
|
||||
运行时长: <span class="font-mono text-blue-600">{{ runningDuration }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 text-sm">
|
||||
<span
|
||||
class="inline-block h-2 w-2 rounded-full"
|
||||
:class="{
|
||||
'bg-green-500': taskStore.wsStatus === 'connected',
|
||||
'bg-yellow-500 animate-pulse': taskStore.wsStatus === 'connecting' || taskStore.wsStatus === 'reconnecting',
|
||||
'bg-red-500': taskStore.wsStatus === 'disconnected',
|
||||
}"
|
||||
/>
|
||||
<span class="text-gray-500">
|
||||
{{
|
||||
taskStore.wsStatus === 'connected' ? '已连接'
|
||||
: taskStore.wsStatus === 'connecting' ? '连接中'
|
||||
: taskStore.wsStatus === 'reconnecting' ? '重连中'
|
||||
: '未连接'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<TabsList>
|
||||
<TabsTrigger value="modeler" class="text-sm">
|
||||
ModelerAgent
|
||||
|
||||
@@ -14,7 +14,8 @@ export const useApiKeyStore = defineStore(
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
});
|
||||
|
||||
/** 建模者模型配置 */
|
||||
@@ -22,7 +23,8 @@ export const useApiKeyStore = defineStore(
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
});
|
||||
|
||||
/** 编码者模型配置 */
|
||||
@@ -30,7 +32,8 @@ export const useApiKeyStore = defineStore(
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
});
|
||||
|
||||
/** 写作者模型配置 */
|
||||
@@ -38,7 +41,8 @@ export const useApiKeyStore = defineStore(
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
});
|
||||
|
||||
/** OpenAlex 邮箱 */
|
||||
@@ -97,25 +101,29 @@ export const useApiKeyStore = defineStore(
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
modelerConfig.value = {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
coderConfig.value = {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
writerConfig.value = {
|
||||
apiKey: "",
|
||||
baseUrl: "",
|
||||
modelId: "",
|
||||
provider: "",
|
||||
apiType: "",
|
||||
contextWindow: 128000,
|
||||
};
|
||||
openalexEmail.value = "";
|
||||
}
|
||||
|
||||
+18
-42
@@ -1,7 +1,6 @@
|
||||
import { getTaskMessages } from "@/apis/commonApi";
|
||||
import { AgentType } from "@/utils/enum";
|
||||
import type {
|
||||
ApprovalMessage,
|
||||
CoderMessage,
|
||||
CoordinatorMessage,
|
||||
InterpreterMessage,
|
||||
@@ -37,8 +36,10 @@ export const useTaskStore = defineStore("task", () => {
|
||||
/** WebSocket 实例 */
|
||||
let ws: TaskWebSocket | null = null;
|
||||
|
||||
/** 审批消息回调(由 ApprovalDialog 注册) */
|
||||
let onApprovalCallback: ((data: ApprovalMessage) => void) | null = null;
|
||||
/** WebSocket 连接状态 */
|
||||
const wsStatus = ref<
|
||||
"connecting" | "connected" | "disconnected" | "reconnecting"
|
||||
>("disconnected");
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
@@ -72,18 +73,10 @@ export const useTaskStore = defineStore("task", () => {
|
||||
return (
|
||||
typeof Reflect.get(payload, "id") === "string" &&
|
||||
typeof msgType === "string" &&
|
||||
["system", "agent", "user", "tool", "approval"].includes(msgType)
|
||||
["system", "agent", "user", "tool"].includes(msgType)
|
||||
);
|
||||
}
|
||||
|
||||
/** 类型守卫:判断是否为审批消息 */
|
||||
function isApprovalMessage(payload: unknown): payload is ApprovalMessage {
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return false;
|
||||
}
|
||||
return Reflect.get(payload, "msg_type") === "approval";
|
||||
}
|
||||
|
||||
/** 设置当前活跃任务 */
|
||||
function setCurrentTask(taskId: string) {
|
||||
currentTaskId.value = taskId;
|
||||
@@ -158,19 +151,19 @@ export const useTaskStore = defineStore("task", () => {
|
||||
const baseUrl = import.meta.env.VITE_WS_URL;
|
||||
const wsUrl = `${baseUrl}/task/${taskId}`;
|
||||
|
||||
ws = new TaskWebSocket(wsUrl, (data) => {
|
||||
// 处理审批消息
|
||||
if (isApprovalMessage(data)) {
|
||||
ws = new TaskWebSocket(
|
||||
wsUrl,
|
||||
(data) => {
|
||||
if (!isMessagePayload(data)) {
|
||||
console.warn("忽略非标准任务消息:", data);
|
||||
return;
|
||||
}
|
||||
appendMessage(taskId, data);
|
||||
onApprovalCallback?.(data);
|
||||
return;
|
||||
}
|
||||
if (!isMessagePayload(data)) {
|
||||
console.warn("忽略非标准任务消息:", data);
|
||||
return;
|
||||
}
|
||||
appendMessage(taskId, data);
|
||||
});
|
||||
},
|
||||
(status) => {
|
||||
wsStatus.value = status;
|
||||
},
|
||||
);
|
||||
// 初始化测试数据(已在上面初始化,这里可以注释掉)
|
||||
// messages.value = messageData as Message[]
|
||||
ws.connect();
|
||||
@@ -319,24 +312,9 @@ export const useTaskStore = defineStore("task", () => {
|
||||
// 如果需要自动连接,可以在这里添加代码
|
||||
// 例如:connectWebSocket('default')
|
||||
|
||||
/** 注册审批消息回调 */
|
||||
function onApproval(callback: (data: ApprovalMessage) => void) {
|
||||
onApprovalCallback = callback;
|
||||
}
|
||||
|
||||
/** 通过 WebSocket 发送用户决策 */
|
||||
function sendDecision(checkpointId: string, decision: { action: string; content?: unknown }) {
|
||||
if (ws) {
|
||||
ws.send({
|
||||
type: "user_decision",
|
||||
checkpoint_id: checkpointId,
|
||||
decision,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages,
|
||||
wsStatus,
|
||||
chatMessages,
|
||||
coordinatorMessages,
|
||||
modelerMessages,
|
||||
@@ -349,7 +327,5 @@ export const useTaskStore = defineStore("task", () => {
|
||||
closeWebSocket,
|
||||
downloadMessages,
|
||||
addUserMessage,
|
||||
onApproval,
|
||||
sendDecision,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -5,3 +5,10 @@ export enum AgentType {
|
||||
CODER = "CoderAgent",
|
||||
WRITER = "WriterAgent",
|
||||
}
|
||||
|
||||
/** LLM API 类型枚举 */
|
||||
export enum ApiType {
|
||||
OPENAI_CHAT = "openai-chat",
|
||||
OPENAI_RESPONSES = "openai-responses",
|
||||
ANTHROPIC = "anthropic",
|
||||
}
|
||||
|
||||
@@ -20,5 +20,7 @@ export interface ModelConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
modelId: string;
|
||||
provider: string;
|
||||
apiType: string;
|
||||
/** 上下文窗口大小(token),用于记忆压缩阈值 */
|
||||
contextWindow?: number;
|
||||
}
|
||||
|
||||
@@ -125,15 +125,6 @@ export interface WriterMessage extends AgentMessage {
|
||||
sub_title?: string;
|
||||
}
|
||||
|
||||
/** HIL 审批消息 */
|
||||
export interface ApprovalMessage extends BaseMessage {
|
||||
msg_type: "approval";
|
||||
checkpoint_id: string;
|
||||
prompt: Record<string, unknown>;
|
||||
options: string[];
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
/** 所有消息类型的联合类型 */
|
||||
export type Message =
|
||||
| SystemMessage
|
||||
@@ -142,5 +133,4 @@ export type Message =
|
||||
| WriterMessage
|
||||
| ModelerMessage
|
||||
| CoordinatorMessage
|
||||
| ToolMessage
|
||||
| ApprovalMessage;
|
||||
| ToolMessage;
|
||||
|
||||
@@ -1,22 +1,57 @@
|
||||
/** WebSocket 消息处理回调函数类型 */
|
||||
type MessageHandler = (data: unknown) => void;
|
||||
|
||||
/** 连接状态变化回调函数类型 */
|
||||
type StatusHandler = (
|
||||
status: "connecting" | "connected" | "disconnected" | "reconnecting",
|
||||
) => void;
|
||||
|
||||
/** 重连配置 */
|
||||
interface ReconnectConfig {
|
||||
/** 最大重试次数 */
|
||||
maxRetries: number;
|
||||
/** 初始重试延迟(毫秒) */
|
||||
initialDelay: number;
|
||||
/** 最大重试延迟(毫秒) */
|
||||
maxDelay: number;
|
||||
}
|
||||
|
||||
/** 任务 WebSocket 连接管理类 */
|
||||
export class TaskWebSocket {
|
||||
private socket: WebSocket | null = null;
|
||||
private url: string;
|
||||
private onMessage: MessageHandler;
|
||||
private onStatus: StatusHandler | null = null;
|
||||
private reconnectConfig: ReconnectConfig;
|
||||
private reconnectAttempts = 0;
|
||||
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private isManualClose = false;
|
||||
|
||||
constructor(url: string, onMessage: MessageHandler) {
|
||||
constructor(
|
||||
url: string,
|
||||
onMessage: MessageHandler,
|
||||
onStatus?: StatusHandler,
|
||||
) {
|
||||
this.url = url;
|
||||
this.onMessage = onMessage;
|
||||
this.onStatus = onStatus ?? null;
|
||||
this.reconnectConfig = {
|
||||
maxRetries: 10,
|
||||
initialDelay: 1000,
|
||||
maxDelay: 30000,
|
||||
};
|
||||
}
|
||||
|
||||
/** 建立 WebSocket 连接 */
|
||||
connect() {
|
||||
this.isManualClose = false;
|
||||
this.notifyStatus("connecting");
|
||||
|
||||
this.socket = new WebSocket(this.url);
|
||||
this.socket.onopen = () => {
|
||||
console.log("WebSocket 连接已建立");
|
||||
this.reconnectAttempts = 0;
|
||||
this.notifyStatus("connected");
|
||||
};
|
||||
this.socket.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
@@ -24,6 +59,12 @@ export class TaskWebSocket {
|
||||
};
|
||||
this.socket.onclose = (event) => {
|
||||
console.log("WebSocket 连接已关闭", event.code, event.reason);
|
||||
this.notifyStatus("disconnected");
|
||||
|
||||
// 非手动关闭时尝试重连
|
||||
if (!this.isManualClose) {
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
};
|
||||
this.socket.onerror = (error) => {
|
||||
console.error("WebSocket 错误:", error);
|
||||
@@ -39,8 +80,54 @@ export class TaskWebSocket {
|
||||
|
||||
/** 关闭连接 */
|
||||
close() {
|
||||
this.isManualClose = true;
|
||||
this.clearReconnectTimer();
|
||||
if (this.socket) {
|
||||
this.socket.close();
|
||||
this.socket = null;
|
||||
}
|
||||
this.notifyStatus("disconnected");
|
||||
}
|
||||
|
||||
/** 安排重连 */
|
||||
private scheduleReconnect() {
|
||||
if (this.reconnectAttempts >= this.reconnectConfig.maxRetries) {
|
||||
console.error(
|
||||
`WebSocket 重连失败,已达到最大重试次数 ${this.reconnectConfig.maxRetries}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifyStatus("reconnecting");
|
||||
|
||||
// 指数退避策略
|
||||
const delay = Math.min(
|
||||
this.reconnectConfig.initialDelay * 2 ** this.reconnectAttempts,
|
||||
this.reconnectConfig.maxDelay,
|
||||
);
|
||||
|
||||
console.log(
|
||||
`WebSocket 将在 ${delay}ms 后重连 (第 ${this.reconnectAttempts + 1} 次)`,
|
||||
);
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectAttempts++;
|
||||
this.connect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
/** 清除重连定时器 */
|
||||
private clearReconnectTimer() {
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 通知连接状态变化 */
|
||||
private notifyStatus(
|
||||
status: "connecting" | "connected" | "disconnected" | "reconnecting",
|
||||
) {
|
||||
this.onStatus?.(status);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user