diff --git a/Co-creation-projects/aug618-Praxis/.env.example b/Co-creation-projects/aug618-Praxis/.env.example
new file mode 100644
index 00000000..2542544d
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/.env.example
@@ -0,0 +1,118 @@
+# Copy this file to .env before running the project.
+
+# -------------------------------------------------
+# Basic runtime flags
+# -------------------------------------------------
+DEBUG=false
+CODE_AGENT_DEBUG=false
+LOG_LEVEL=INFO
+NO_COLOR=
+TEMPERATURE=0.7
+MAX_TOKENS=
+
+# -------------------------------------------------
+# Code Agent core config
+# -------------------------------------------------
+HELLOAGENTS_DIR=.helloagents
+CODE_AGENT_STATE_DIR=.helloagents
+CODE_AGENT_MAX_REACT_STEPS=20
+CODE_AGENT_MAX_STEPS=20
+CODE_AGENT_TERMINAL_TIMEOUT=60
+CODE_AGENT_PATCH_MAX_FILES=10
+CODE_AGENT_PATCH_MAX_LINES=800
+
+# -------------------------------------------------
+# Observability
+# -------------------------------------------------
+CODE_AGENT_LOG_DIR=
+CODE_AGENT_SESSION_ID=
+
+# -------------------------------------------------
+# LLM generic config
+# -------------------------------------------------
+LLM_MODEL_ID=
+LLM_API_KEY=
+LLM_BASE_URL=
+LLM_TIMEOUT=60
+
+# -------------------------------------------------
+# Provider-specific config
+# -------------------------------------------------
+OPENAI_API_KEY=
+DEEPSEEK_API_KEY=
+DASHSCOPE_API_KEY=
+DASHSCOPE_BASE_URL=
+MODELSCOPE_API_KEY=
+KIMI_API_KEY=
+MOONSHOT_API_KEY=
+ZHIPU_API_KEY=
+GLM_API_KEY=
+OLLAMA_HOST=http://localhost:11434/v1
+OLLAMA_API_KEY=ollama
+VLLM_HOST=http://localhost:8000/v1
+VLLM_API_KEY=vllm
+
+# -------------------------------------------------
+# MCP
+# -------------------------------------------------
+CODE_AGENT_QUIET=1
+MCP_MONITOR_COMMAND=
+MCP_PLAYWRIGHT_COMMAND=
+
+# -------------------------------------------------
+# Skills
+# -------------------------------------------------
+CODE_AGENT_SKILLS_DIR=
+CODE_AGENT_ENABLE_SKILLS_INDEX=1
+CODE_AGENT_AUTO_LOAD_FIND_SKILLS=1
+
+# -------------------------------------------------
+# TUI logo
+# -------------------------------------------------
+CODE_AGENT_LOGO=
+CODE_AGENT_LOGO_MODE=image
+CODE_AGENT_LOGO_VISIBILITY=once
+CODE_AGENT_LOGO_SPLASH_SECONDS=2
+CODE_AGENT_LOGO_ANIMATE=1
+CODE_AGENT_LOGO_MAX_HEIGHT=
+CODE_AGENT_LOGO_DISABLE_HEIGHT_LIMIT=0
+CODE_AGENT_LOGO_WIDTH=
+CODE_AGENT_LOGO_DOT_CHAR=.
+CODE_AGENT_LOGO_DOT_ASPECT=0.55
+
+# -------------------------------------------------
+# Search backends
+# -------------------------------------------------
+TAVILY_API_KEY=
+SERPAPI_API_KEY=
+
+# -------------------------------------------------
+# Vector / memory
+# -------------------------------------------------
+CLIP_MODEL=openai/clip-vit-base-patch32
+CLAP_MODEL=laion/clap-htsat-unfused
+QDRANT_URL=
+QDRANT_API_KEY=
+QDRANT_COLLECTION=hello_agents_vectors
+QDRANT_DISTANCE=cosine
+QDRANT_VECTOR_SIZE=384
+QDRANT_TIMEOUT=30
+QDRANT_HNSW_M=32
+QDRANT_HNSW_EF_CONSTRUCT=256
+QDRANT_SEARCH_EF=128
+QDRANT_SEARCH_EXACT=0
+EMBED_MODEL_TYPE=dashscope
+EMBED_MODEL_NAME=
+EMBED_API_KEY=
+EMBED_BASE_URL=
+
+# -------------------------------------------------
+# Neo4j
+# -------------------------------------------------
+NEO4J_URI=bolt://localhost:7687
+NEO4J_USERNAME=neo4j
+NEO4J_PASSWORD=hello-agents-password
+NEO4J_DATABASE=neo4j
+NEO4J_MAX_CONNECTION_LIFETIME=3600
+NEO4J_MAX_CONNECTION_POOL_SIZE=50
+NEO4J_CONNECTION_TIMEOUT=60
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/.gitignore b/Co-creation-projects/aug618-Praxis/.gitignore
new file mode 100644
index 00000000..9d0ec94e
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/.gitignore
@@ -0,0 +1,76 @@
+# Python
+__pycache__/
+*.py[cod]
+*$py.class
+*.so
+.Python
+venv/
+env/
+ENV/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# Environment
+.env
+.env.local
+.venv
+
+# IDE
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# Logs
+logs/
+*.log
+
+# Database
+*.db
+*.sqlite
+*.sqlite3
+
+# Data
+data/
+*.csv
+*.json
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Testing
+.pytest_cache/
+.coverage
+htmlcov/
+test/
+
+# Jupyter
+.ipynb_checkpoints/
+
+# Model files
+*.pth
+*.pt
+*.ckpt
+models/checkpoints/
+
+# Submission helper files (for reference only, not to be committed to hello-agents)
+PR.md
+提交文件说明.md
+
+.helloagents/
+.agents/
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/.python-version b/Co-creation-projects/aug618-Praxis/.python-version
new file mode 100644
index 00000000..e4fba218
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/.python-version
@@ -0,0 +1 @@
+3.12
diff --git a/Co-creation-projects/aug618-Praxis/LICENSE b/Co-creation-projects/aug618-Praxis/LICENSE
new file mode 100644
index 00000000..c55270dd
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 aug618
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/README.md b/Co-creation-projects/aug618-Praxis/README.md
new file mode 100644
index 00000000..d8ecf3d1
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/README.md
@@ -0,0 +1,192 @@
+# Praxis
+
+> 基于 YYHDBL-HelloCodeAgentCli 与 Hello-Agents 框架继续二次开发的本地代码仓库智能助手
+
+
+
+

+
+[](https://www.python.org/)
+[](https://github.com/datawhalechina/hello-agents)
+[](LICENSE)
+
+
+
+## 📝 项目简介
+
+Praxis 是一个面向本地代码仓库的 AI Code Agent 项目,直接基于 YYHDBL-HelloCodeAgentCli 继续做二次开发,并沿用其背后的 Hello-Agents 框架能力,目标是提供类似 Claude Code / Codex 的本地交互体验。
+
+这个项目主要解决本地代码仓库分析、修改和验证流程割裂的问题,把代码理解、工具调用、补丁生成、修改确认和交互展示串成一个完整闭环。
+
+它的特色在于:
+- 同时提供 CLI 与 TUI 两套交互方式
+- 支持 ReAct 多步推理与工具协同
+- 支持标准补丁识别、确认、备份与落盘
+- 支持 Skills 渐进加载与 MCP 扩展工具接入
+
+它适用于:
+- 本地代码仓库探索与结构分析
+- 小范围代码修复与重构
+- 代码审查辅助
+- 演示 Agent 工程化落地能力
+
+## ✨ 核心功能
+
+- [x] 本地代码仓库问答与结构分析
+- [x] ReAct 多步推理与工具调用
+- [x] 标准补丁识别、确认、备份与落盘
+- [x] CLI 与 TUI 双交互界面
+- [x] 会话日志、导出与统计
+- [x] Skills 机制接入,支持按需加载 SOP
+- [x] MCP 扩展接入,支持外部工具服务器
+- [x] 文件、目录、图片引用与 OCR / 多模态协同
+
+## 🛠️ 技术栈
+
+- Hello-Agents 0.2.7
+- YYHDBL-HelloCodeAgentCli 二次开发基础
+- ReAct Agent 工作流
+- Python 3.12+
+- Textual TUI
+- OpenAI Compatible LLM API
+- MCP 工具扩展
+- Skills 渐进式知识加载
+
+## 🚀 快速开始
+
+### 环境要求
+
+- Python 3.12+
+- uv 或 pip
+- 可访问的 OpenAI Compatible 模型服务
+
+### 安装依赖
+
+推荐使用 uv:
+
+```bash
+git clone https://github.com/aug618/Praxis.git
+cd Praxis
+uv venv
+uv sync
+```
+
+如果使用 pip:
+
+```bash
+python -m venv .venv
+.venv\Scripts\activate
+pip install -r requirements.txt
+```
+
+### 配置API密钥
+
+```bash
+copy .env.example .env
+```
+
+编辑 .env 文件,填入你的模型配置,例如:
+
+```dotenv
+LLM_MODEL_ID=glm-4.7
+LLM_BASE_URL=https://open.bigmodel.cn/api/paas/v4
+ZHIPU_API_KEY=your_api_key
+
+HELLOAGENTS_DIR=.helloagents
+CODE_AGENT_MAX_REACT_STEPS=20
+LLM_TIMEOUT=60
+```
+
+如果你使用 DeepSeek、Qwen、Ollama 或其他 OpenAI Compatible 后端,只需替换对应的模型名、Base URL 和 API Key。
+
+### 运行项目
+
+项目当前以 Python 脚本形式运行,不依赖 Jupyter Notebook。
+
+启动 CLI:
+
+```bash
+python -m code_agent.hello_code_cli --repo .
+```
+
+启动 TUI:
+
+```bash
+python -m code_agent.hello_code_tui --repo .
+```
+
+如果使用 uv:
+
+```bash
+uv run python -m code_agent.hello_code_cli --repo .
+uv run python -m code_agent.hello_code_tui --repo .
+```
+
+## 📖 使用示例
+
+下面是几个典型交互示例:
+
+```text
+@dir(core/, tools/) 先告诉我这两个模块分别负责什么,再指出主要入口
+```
+
+```text
+@file(core/config.py) 这里有弃用警告,帮我用最小改动修复
+```
+
+```text
+修复完之后跑 pytest -q,若失败就根据输出继续改
+```
+
+CLI 演示截图:
+
+
+
+TUI 动图演示:
+
+
+
+## 🎯 项目亮点
+
+- 本地仓库优先:围绕本地代码库分析、修改、验证设计,不依赖远端 SaaS 工作流。
+- 安全修改闭环:通过标准补丁格式执行代码修改,落盘前支持确认与备份。
+- 双界面体验:CLI 适合快速问答,TUI 适合长会话和过程观察。
+- 扩展能力完整:不仅支持内置工具,还支持 Skills 和 MCP 两类扩展机制。
+- 工程化更完整:包含日志、会话导出、计划生成、Todo 跟踪等能力。
+
+## 📊 性能评估
+
+当前项目以功能完整性和交互体验为主,尚未形成统一的量化 benchmark,现阶段可确认的结果包括:
+
+- 已完成 CLI 与 TUI 两套可运行入口
+- 已具备本地代码仓库分析与补丁执行闭环
+- 已支持会话日志、导出、Todo、Skills 与 MCP 扩展能力
+- 后续可补充任务成功率、平均响应时间和补丁应用成功率等指标
+
+## 🔮 未来计划
+
+- [ ] 支持会话恢复与断点续传
+- [ ] 继续细化终端工具为更原子的命令工具
+- [ ] 重构 Note Tool 与 Memory Tool 的交互方式
+- [ ] 完善测试用例与自动化验证流程
+- [ ] 增加更多可直接启用的 MCP 工具模板
+
+## 🤝 贡献指南
+
+欢迎提出 Issue 和 Pull Request。
+
+## 📄 许可证
+
+MIT License
+
+## 👤 作者
+
+- GitHub: [@aug618](https://github.com/aug618)
+- 二次开发来源:YYHDBL-HelloCodeAgentCli
+- 上游项目仓库:https://github.com/aug618/Praxis
+
+## 🙏 致谢
+
+感谢 Datawhale 社区和 Hello-Agents 项目。
+
+同时感谢 YYHDBL-HelloCodeAgentCli 项目为本项目提供二次开发基础。
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/__init__.py b/Co-creation-projects/aug618-Praxis/__init__.py
new file mode 100644
index 00000000..c38a0339
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/__init__.py
@@ -0,0 +1,66 @@
+"""
+HelloAgents - 灵活、可扩展的多智能体框架
+
+基于OpenAI原生API构建,提供简洁高效的智能体开发体验。
+"""
+
+# 配置第三方库的日志级别,减少噪音
+import logging
+logging.getLogger("httpx").setLevel(logging.WARNING)
+logging.getLogger("qdrant_client").setLevel(logging.WARNING)
+logging.getLogger("urllib3").setLevel(logging.WARNING)
+logging.getLogger("neo4j").setLevel(logging.WARNING)
+logging.getLogger("neo4j.notifications").setLevel(logging.WARNING)
+
+# from .version import __version__, __author__, __email__, __description__
+
+# 核心组件
+from .core.llm import HelloAgentsLLM
+from .core.config import Config
+from .core.message import Message
+from .core.exceptions import HelloAgentsException
+
+# Agent实现
+from .agents.simple_agent import SimpleAgent
+from .agents.react_agent import ReActAgent
+from .agents.reflection_agent import ReflectionAgent
+from .agents.plan_solve_agent import PlanAndSolveAgent
+
+# 工具系统
+from .tools.registry import ToolRegistry, global_registry
+from .tools.builtin.search import SearchTool, search
+# from .tools.builtin.calculator import CalculatorTool, calculate
+from .tools.chain import ToolChain, ToolChainManager
+from .tools.async_executor import AsyncToolExecutor
+
+__all__ = [
+ # 版本信息
+ "__version__",
+ "__author__",
+ "__email__",
+ "__description__",
+
+ # 核心组件
+ "HelloAgentsLLM",
+ "Config",
+ "Message",
+ "HelloAgentsException",
+
+ # Agent范式
+ "SimpleAgent",
+ "ReActAgent",
+ "ReflectionAgent",
+ "PlanAndSolveAgent",
+
+ # 工具系统
+ "ToolRegistry",
+ "global_registry",
+ "SearchTool",
+ "search",
+ # "CalculatorTool",
+ # "calculate",
+ "ToolChain",
+ "ToolChainManager",
+ "AsyncToolExecutor",
+]
+
diff --git a/Co-creation-projects/aug618-Praxis/agents/__init__.py b/Co-creation-projects/aug618-Praxis/agents/__init__.py
new file mode 100644
index 00000000..32f62de8
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/agents/__init__.py
@@ -0,0 +1,7 @@
+"""Agent implementations.
+
+Keep this module lightweight: avoid importing optional dependencies at import time.
+Import concrete agents directly, e.g. `from agents.simple_agent import SimpleAgent`.
+"""
+
+__all__ = ["SimpleAgent", "ReActAgent", "ReflectionAgent", "PlanAndSolveAgent"]
diff --git a/Co-creation-projects/aug618-Praxis/agents/plan_solve_agent.py b/Co-creation-projects/aug618-Praxis/agents/plan_solve_agent.py
new file mode 100644
index 00000000..96dc36c9
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/agents/plan_solve_agent.py
@@ -0,0 +1,201 @@
+"""Plan and Solve Agent实现 - 分解规划与逐步执行的智能体"""
+
+import ast
+from typing import Optional, List, Dict
+from core.agent import Agent
+from core.llm import HelloAgentsLLM
+from core.config import Config
+from core.message import Message
+
+# 默认规划器提示词模板
+DEFAULT_PLANNER_PROMPT = """
+你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
+请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
+你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。
+
+问题: {question}
+
+请严格按照以下格式输出你的计划:
+```python
+["步骤1", "步骤2", "步骤3", ...]
+```
+"""
+
+# 默认执行器提示词模板
+DEFAULT_EXECUTOR_PROMPT = """
+你是一位顶级的AI执行专家。你的任务是严格按照给定的计划,一步步地解决问题。
+你将收到原始问题、完整的计划、以及到目前为止已经完成的步骤和结果。
+请你专注于解决"当前步骤",并仅输出该步骤的最终答案,不要输出任何额外的解释或对话。
+
+# 原始问题:
+{question}
+
+# 完整计划:
+{plan}
+
+# 历史步骤与结果:
+{history}
+
+# 当前步骤:
+{current_step}
+
+请仅输出针对"当前步骤"的回答:
+"""
+
+class Planner:
+ """规划器 - 负责将复杂问题分解为简单步骤"""
+
+ def __init__(self, llm_client: HelloAgentsLLM, prompt_template: Optional[str] = None):
+ self.llm_client = llm_client
+ self.prompt_template = prompt_template if prompt_template else DEFAULT_PLANNER_PROMPT
+
+ def plan(self, question: str, **kwargs) -> List[str]:
+ """
+ 生成执行计划
+
+ Args:
+ question: 要解决的问题
+ **kwargs: LLM调用参数
+
+ Returns:
+ 步骤列表
+ """
+ prompt = self.prompt_template.format(question=question)
+ messages = [{"role": "user", "content": prompt}]
+
+ print("--- 正在生成计划 ---")
+ response_text = self.llm_client.invoke(messages, **kwargs) or ""
+ print(f"✅ 计划已生成:\n{response_text}")
+
+ try:
+ # 提取Python代码块中的列表
+ plan_str = response_text.split("```python")[1].split("```")[0].strip()
+ plan = ast.literal_eval(plan_str)
+ return plan if isinstance(plan, list) else []
+ except (ValueError, SyntaxError, IndexError) as e:
+ print(f"❌ 解析计划时出错: {e}")
+ print(f"原始响应: {response_text}")
+ return []
+ except Exception as e:
+ print(f"❌ 解析计划时发生未知错误: {e}")
+ return []
+
+class Executor:
+ """执行器 - 负责按计划逐步执行"""
+
+ def __init__(self, llm_client: HelloAgentsLLM, prompt_template: Optional[str] = None):
+ self.llm_client = llm_client
+ self.prompt_template = prompt_template if prompt_template else DEFAULT_EXECUTOR_PROMPT
+
+ def execute(self, question: str, plan: List[str], **kwargs) -> str:
+ """
+ 按计划执行任务
+
+ Args:
+ question: 原始问题
+ plan: 执行计划
+ **kwargs: LLM调用参数
+
+ Returns:
+ 最终答案
+ """
+ history = ""
+ final_answer = ""
+
+ print("\n--- 正在执行计划 ---")
+ for i, step in enumerate(plan, 1):
+ print(f"\n-> 正在执行步骤 {i}/{len(plan)}: {step}")
+ prompt = self.prompt_template.format(
+ question=question,
+ plan=plan,
+ history=history if history else "无",
+ current_step=step
+ )
+ messages = [{"role": "user", "content": prompt}]
+
+ response_text = self.llm_client.invoke(messages, **kwargs) or ""
+
+ history += f"步骤 {i}: {step}\n结果: {response_text}\n\n"
+ final_answer = response_text
+ print(f"✅ 步骤 {i} 已完成,结果: {final_answer}")
+
+ return final_answer
+
+class PlanAndSolveAgent(Agent):
+ """
+ Plan and Solve Agent - 分解规划与逐步执行的智能体
+
+ 这个Agent能够:
+ 1. 将复杂问题分解为简单步骤
+ 2. 按照计划逐步执行
+ 3. 维护执行历史和上下文
+ 4. 得出最终答案
+
+ 特别适合多步骤推理、数学问题、复杂分析等任务。
+ """
+
+ def __init__(
+ self,
+ name: str,
+ llm: HelloAgentsLLM,
+ system_prompt: Optional[str] = None,
+ config: Optional[Config] = None,
+ custom_prompts: Optional[Dict[str, str]] = None
+ ):
+ """
+ 初始化PlanAndSolveAgent
+
+ Args:
+ name: Agent名称
+ llm: LLM实例
+ system_prompt: 系统提示词
+ config: 配置对象
+ custom_prompts: 自定义提示词模板 {"planner": "", "executor": ""}
+ """
+ super().__init__(name, llm, system_prompt, config)
+
+ # 设置提示词模板:用户自定义优先,否则使用默认模板
+ if custom_prompts:
+ planner_prompt = custom_prompts.get("planner")
+ executor_prompt = custom_prompts.get("executor")
+ else:
+ planner_prompt = None
+ executor_prompt = None
+
+ self.planner = Planner(self.llm, planner_prompt)
+ self.executor = Executor(self.llm, executor_prompt)
+
+ def run(self, input_text: str, **kwargs) -> str:
+ """
+ 运行Plan and Solve Agent
+
+ Args:
+ input_text: 要解决的问题
+ **kwargs: 其他参数
+
+ Returns:
+ 最终答案
+ """
+ print(f"\n🤖 {self.name} 开始处理问题: {input_text}")
+
+ # 1. 生成计划
+ plan = self.planner.plan(input_text, **kwargs)
+ if not plan:
+ final_answer = "无法生成有效的行动计划,任务终止。"
+ print(f"\n--- 任务终止 ---\n{final_answer}")
+
+ # 保存到历史记录
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(final_answer, "assistant"))
+
+ return final_answer
+
+ # 2. 执行计划
+ final_answer = self.executor.execute(input_text, plan, **kwargs)
+ print(f"\n--- 任务完成 ---\n最终答案: {final_answer}")
+
+ # 保存到历史记录
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(final_answer, "assistant"))
+
+ return final_answer
diff --git a/Co-creation-projects/aug618-Praxis/agents/react_agent.py b/Co-creation-projects/aug618-Praxis/agents/react_agent.py
new file mode 100644
index 00000000..4a022b39
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/agents/react_agent.py
@@ -0,0 +1,465 @@
+"""ReAct Agent实现 - 推理与行动结合的智能体"""
+
+import re
+from typing import Optional, List, Tuple, Callable, Dict, Any
+import json
+import os
+from core.agent import Agent
+from core.llm import HelloAgentsLLM
+from core.config import Config
+from core.message import Message
+from tools.registry import ToolRegistry
+from utils.cli_ui import Spinner, c, PRIMARY, ACCENT, INFO, hr, log_tool_event, clamp_text
+
+# 默认ReAct提示词模板
+DEFAULT_REACT_PROMPT = """你是一个具备推理和行动能力的AI助手。你可以通过思考分析问题,然后调用合适的工具来获取信息,最终给出准确的答案。
+
+## 可用工具
+{tools}
+
+## 工作流程
+请严格按照以下格式进行回应,每次只能执行一个步骤:
+
+**Thought:** 分析当前问题,思考需要什么信息或采取什么行动。
+**Action:** 选择一个行动,格式必须是以下之一:
+- `{{tool_name}}[{{tool_input}}]` - 调用指定工具
+- `Finish[最终答案]` - 当你有足够信息给出最终答案时
+
+## 重要提醒
+1. 每次回应必须包含Thought和Action两部分
+2. 工具调用的格式必须严格遵循:工具名[参数]
+3. 只有当你确信有足够信息回答问题时,才使用Finish
+4. 如果工具返回的信息不够,继续使用其他工具或相同工具的不同参数
+
+## 当前任务
+**Question:** {question}
+
+## 执行历史
+{history}
+
+现在开始你的推理和行动:"""
+
+class ReActAgent(Agent):
+ """
+ ReAct (Reasoning and Acting) Agent
+
+ 结合推理和行动的智能体,能够:
+ 1. 分析问题并制定行动计划
+ 2. 调用外部工具获取信息
+ 3. 基于观察结果进行推理
+ 4. 迭代执行直到得出最终答案
+
+ 这是一个经典的Agent范式,特别适合需要外部信息的任务。
+ """
+
+ def __init__(
+ self,
+ name: str,
+ llm: HelloAgentsLLM,
+ tool_registry: Optional[ToolRegistry] = None,
+ system_prompt: Optional[str] = None,
+ config: Optional[Config] = None,
+ max_steps: int = 5,
+ custom_prompt: Optional[str] = None,
+ observation_summarizer: Optional[Callable[[str, str, str], str]] = None,
+ summarize_threshold_chars: int = 2000,
+ finalize_on_max_steps: bool = True,
+ early_stop_on_repeat: bool = True,
+ repeat_action_threshold: int = 2,
+ ):
+ """
+ 初始化ReActAgent
+
+ Args:
+ name: Agent名称
+ llm: LLM实例
+ tool_registry: 工具注册表(可选,如果不提供则创建空的工具注册表)
+ system_prompt: 系统提示词
+ config: 配置对象
+ max_steps: 最大执行步数
+ custom_prompt: 自定义提示词模板
+ """
+ super().__init__(name, llm, system_prompt, config)
+
+ # 如果没有提供tool_registry,创建一个空的
+ if tool_registry is None:
+ self.tool_registry = ToolRegistry()
+ else:
+ self.tool_registry = tool_registry
+
+ self.max_steps = max_steps
+ self.current_history: List[str] = []
+ self.last_trace: List[Dict[str, Any]] = []
+ self.observation_summarizer = observation_summarizer
+ self.summarize_threshold_chars = summarize_threshold_chars
+ self.finalize_on_max_steps = finalize_on_max_steps
+ self.early_stop_on_repeat = early_stop_on_repeat
+ self.repeat_action_threshold = repeat_action_threshold
+
+ # 设置提示词模板:用户自定义优先,否则使用默认模板
+ self.prompt_template = custom_prompt if custom_prompt else DEFAULT_REACT_PROMPT
+
+ def add_tool(self, tool):
+ """
+ 添加工具到工具注册表
+ 支持MCP工具的自动展开
+
+ Args:
+ tool: 工具实例(可以是普通Tool或MCPTool)
+ """
+ # 检查是否是MCP工具
+ if hasattr(tool, 'auto_expand') and tool.auto_expand:
+ # MCP工具会自动展开为多个工具
+ if hasattr(tool, '_available_tools') and tool._available_tools:
+ for mcp_tool in tool._available_tools:
+ # 创建包装工具
+ from tools.base import Tool
+ wrapped_tool = Tool(
+ name=f"{tool.name}_{mcp_tool['name']}",
+ description=mcp_tool.get('description', ''),
+ func=lambda input_text, t=tool, tn=mcp_tool['name']: t.run({
+ "action": "call_tool",
+ "tool_name": tn,
+ "arguments": {"input": input_text}
+ })
+ )
+ self.tool_registry.register_tool(wrapped_tool)
+ print(f"✅ MCP工具 '{tool.name}' 已展开为 {len(tool._available_tools)} 个独立工具")
+ else:
+ self.tool_registry.register_tool(tool)
+ else:
+ self.tool_registry.register_tool(tool)
+
+ def run(self, input_text: str, **kwargs) -> str:
+ """
+ 运行ReAct Agent
+
+ Args:
+ input_text: 用户问题
+ **kwargs: 其他参数
+
+ Returns:
+ 最终答案
+ """
+ # Optional multimodal attachments (OpenAI-compatible content parts).
+ # Example: [{"type":"image_url","image_url":{"url":"data:image/png;base64,..."}}]
+ attachments = kwargs.pop("attachments", None)
+ self.current_history = []
+ self.last_trace = []
+ current_step = 0
+
+ # Avoid dumping huge stitched prompts to console (CLI UX)
+ preview = input_text.replace("\n", " ")
+ if len(preview) > 160:
+ preview = preview[:160] + "..."
+ # print("\n" + hr("=", 80))
+ # print(c(f"🤖 {self.name}", PRIMARY) + " " + c(f"{preview}", INFO))
+ # print(hr("=", 80))
+
+ repeat_count = 0
+ last_action_sig: Optional[str] = None
+
+ while current_step < self.max_steps:
+ current_step += 1
+ print(c(f"\n--- 当轮需求所处步骤:{current_step}/{self.max_steps} (单次需求最多执行{self.max_steps}步) ---", ACCENT))
+
+ # 构建提示词
+ tools_desc = self.tool_registry.get_tools_description()
+ history_str = "\n".join(self.current_history)
+ prompt = self.prompt_template.format(
+ tools=tools_desc,
+ question=input_text,
+ history=history_str
+ )
+
+ # 调用LLM(支持多模态:prompt + images)
+ user_content: Any = prompt
+ if attachments:
+ user_content = [{"type": "text", "text": prompt}, *list(attachments)]
+ messages = [{"role": "user", "content": user_content}]
+ spinner = Spinner("奶浓正在思考...")
+ spinner.start()
+ response_text = self.llm.invoke(messages, **kwargs)
+ spinner.stop()
+
+ if not response_text:
+ print("❌ 错误:LLM未能返回有效响应。")
+ break
+
+ # 解析输出
+ thought, action = self._parse_output(response_text)
+
+ if thought:
+ print()
+ print(c("奶浓的思考:", INFO), thought)
+ print()
+
+
+ if not action:
+ # One forced retry: ask model to rewrite in strict format (helps for greetings / bilingual models)
+ try:
+ repair_sys = (
+ "You MUST output exactly two lines:\n"
+ "Thought: ...\n"
+ "Action: tool_name[tool_input] OR Finish[final answer]\n"
+ "No extra text. No markdown headers."
+ )
+ repair_user = f"Rewrite the following into the required two-line format:\n\n{response_text}"
+ spinner = Spinner("Repairing format…")
+ spinner.start()
+ repair_user_content: Any = repair_user
+ if attachments:
+ repair_user_content = [{"type": "text", "text": repair_user}, *list(attachments)]
+ repaired = self.llm.invoke(
+ [{"role": "system", "content": repair_sys}, {"role": "user", "content": repair_user_content}],
+ max_tokens=200,
+ )
+ spinner.stop()
+ thought, action = self._parse_output(repaired or "")
+ except Exception:
+ pass
+
+ if not action:
+ print("⚠️ 警告:未能解析出有效的Action,流程终止。")
+ break
+
+ # 检查是否完成
+ if action.startswith("Finish"):
+ final_answer = self._parse_action_input(action)
+ print()
+ print(c("奶浓认为是这样的:", PRIMARY))
+ print(final_answer)
+
+ # 保存到历史记录
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(final_answer, "assistant"))
+
+ return final_answer
+
+ # 执行工具调用
+ tool_name, tool_input = self._parse_action(action)
+ if not tool_name or tool_input is None:
+ self.current_history.append("Observation: 无效的Action格式,请检查。")
+ continue
+
+ # Cursor-like gating: tools require human confirmation before execution.
+ # Default: confirm terminal commands (can extend via env).
+ confirm_tools = os.getenv("CODE_AGENT_CONFIRM_TOOLS", "terminal").strip()
+ confirm_set = {t.strip() for t in confirm_tools.split(",") if t.strip()}
+ if tool_name in confirm_set:
+ payload = {
+ "tool": tool_name,
+ "tool_input": tool_input,
+ "thought": thought,
+ "step": current_step,
+ }
+ # Return a special marker that UI can intercept and ask user to confirm/deny.
+ return (
+ f"需要用户确认后才能执行工具:{tool_name}\n"
+ f"拟执行输入:{tool_input}\n\n"
+ f"[[CONFIRM_TOOL]]{json.dumps(payload, ensure_ascii=False)}[[/CONFIRM_TOOL]]"
+ )
+
+ #log_tool_event(tool_name, tool_input)
+
+ # 调用工具
+ observation = self.tool_registry.execute_tool(tool_name, tool_input)
+ observation_full = observation
+ observation_summary = None
+ if (
+ self.observation_summarizer is not None
+ and isinstance(observation, str)
+ and len(observation) > self.summarize_threshold_chars
+ ):
+ try:
+ observation_summary = self.observation_summarizer(tool_name, tool_input, observation)
+ if observation_summary and isinstance(observation_summary, str):
+ observation = observation_summary.strip() + "\n...truncated...\n"
+ except Exception:
+ # fall back to raw observation
+ pass
+
+ #log_tool_event(f"{tool_name} result", clamp_text(str(observation), limit=6000))
+
+ # 提前终止:重复相同 action 且无明显进展
+ action_sig = f"{tool_name}|{tool_input}".strip()
+ if self.early_stop_on_repeat:
+ if last_action_sig == action_sig:
+ repeat_count += 1
+ else:
+ repeat_count = 0
+ last_action_sig = action_sig
+
+ if repeat_count >= self.repeat_action_threshold:
+ self.current_history.append("Observation: 已检测到重复行动,建议停止继续工具调用并给出当前能提供的结论/下一步。")
+ break
+
+ # 更新历史
+ self.current_history.append(f"Action: {action}")
+ self.current_history.append(f"Observation: {observation}")
+ self.last_trace.append(
+ {
+ "action": action,
+ "tool_name": tool_name,
+ "tool_input": tool_input,
+ "observation_full_len": len(observation_full) if isinstance(observation_full, str) else None,
+ "observation_summary": observation_summary,
+ }
+ )
+
+ # 未在循环内 Finish:进行兜底收敛
+ if self.finalize_on_max_steps:
+ try:
+ tools_desc = self.tool_registry.get_tools_description()
+ history_str = "\n".join(self.current_history[-24:])
+ finalize_prompt = (
+ "你是一个 ReAct 代理的最终收敛器。现在工具调用阶段结束了。"
+ "请基于已有的 Thought/Action/Observation 历史,给出一个尽可能有用的最终回答。"
+ "要求:\n"
+ "1) 不要再调用工具\n"
+ "2) 明确已完成的证据/发现\n"
+ "3) 如果信息不足,说清楚缺少什么,并给出下一步最小化建议(1-3条)\n"
+ )
+ final_user_text = f"Question:\n{input_text}\n\nTools:\n{tools_desc}\n\nTrace:\n{history_str}"
+ final_user_content: Any = final_user_text
+ if attachments:
+ final_user_content = [{"type": "text", "text": final_user_text}, *list(attachments)]
+ messages = [
+ {"role": "system", "content": finalize_prompt},
+ {"role": "user", "content": final_user_content},
+ ]
+ final_answer = self.llm.invoke(messages, max_tokens=600)
+ if final_answer:
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(final_answer, "assistant"))
+ return final_answer
+ except Exception:
+ pass
+
+ print("⏰ 已达到最大步数,流程终止。")
+ final_answer = "抱歉,我无法在限定步数内完成这个任务。你可以缩小范围或指定目标文件/模块。"
+
+ # 保存到历史记录
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(final_answer, "assistant"))
+
+ return final_answer
+
+ def _parse_output(self, text: str) -> Tuple[Optional[str], Optional[str]]:
+ """解析LLM输出,提取思考和行动。
+
+ 兼容常见变体:
+ - Thought/Action 的全角冒号(:)
+ - 中文标签:思考/行动
+ - Markdown 强调:**Thought:** / **Action:**
+ """
+ # Normalize to make regex easier
+ t = (text or "").strip()
+
+ # Primary: strict 2-line format, allow markdown markers and fullwidth colon
+ m = re.search(
+ r"(?:\*\*)?(Thought|思考)(?:\*\*)?\s*[::]\s*(.*?)\n(?:\*\*)?(Action|行动)(?:\*\*)?\s*[::]\s*(.*)\s*$",
+ t,
+ flags=re.DOTALL,
+ )
+ if m:
+ thought = m.group(2).strip()
+ action = m.group(4).strip()
+ return thought or None, action or None
+
+ # Fallback: find first Thought-like line and first Action-like line anywhere
+ thought_match = re.search(r"(?:\*\*)?(Thought|思考)(?:\*\*)?\s*[::]\s*(.*)", t)
+ action_match = re.search(r"(?:\*\*)?(Action|行动)(?:\*\*)?\s*[::]\s*(.*)", t)
+ thought = thought_match.group(2).strip() if thought_match else None
+ action_raw = action_match.group(2).strip() if action_match else None
+
+ # 关键修复:如果 action 中包含另一个 Thought/Action/Observation,截断到该位置
+ # 防止模型一次输出多个 Thought/Action 循环时,把后续内容都当作第一个 Action 的输入
+ if action_raw:
+ stop_patterns = [
+ r"\nThought:", r"\n思考:", r"\nAction:", r"\n行动:",
+ r"\nObservation:", r"\n观察:", r"\n\*\*Thought", r"\n\*\*Action",
+ ]
+ earliest_stop = len(action_raw)
+ for pat in stop_patterns:
+ m = re.search(pat, action_raw, re.IGNORECASE)
+ if m and m.start() < earliest_stop:
+ earliest_stop = m.start()
+ action_raw = action_raw[:earliest_stop].strip()
+
+ return thought, action_raw
+
+ def _parse_action(self, action_text: str) -> Tuple[Optional[str], Optional[str]]:
+ """解析行动文本,提取工具名称和输入
+
+ 使用括号匹配算法而非贪婪正则,正确处理嵌套 JSON。
+ """
+ # 先找工具名
+ name_match = re.match(r"(\w+)\[", action_text)
+ if not name_match:
+ return None, None
+
+ tool_name = name_match.group(1)
+ start = name_match.end() - 1 # '[' 的位置
+
+ # 使用括号匹配找到对应的 ']'
+ depth = 0
+ in_string = False
+ escape = False
+ end_pos = None
+
+ for i, c in enumerate(action_text[start:], start):
+ if escape:
+ escape = False
+ continue
+ if c == '\\' and in_string:
+ escape = True
+ continue
+ if c == '"' and not escape:
+ in_string = not in_string
+ continue
+ if in_string:
+ continue
+ if c == '[':
+ depth += 1
+ elif c == ']':
+ depth -= 1
+ if depth == 0:
+ end_pos = i
+ break
+
+ if end_pos is not None:
+ tool_input = action_text[start + 1:end_pos]
+ return tool_name, tool_input
+
+ # fallback: 如果括号不匹配,尝试简单正则(不跨行)
+ # 注意:不使用 re.DOTALL,这样 . 不会匹配换行符
+ match = re.match(r"(\w+)\[([^\n]*)\]", action_text)
+ if match:
+ return match.group(1), match.group(2)
+
+ return None, None
+
+ def _parse_action_input(self, action_text: str) -> str:
+ """解析行动输入
+
+ 兼容多种 Finish 书写:
+ - Finish[...]
+ - Finish:... / Finish: ...(无方括号)
+ - Finish\n(换行后直接给内容/补丁)
+ """
+ # 规范格式:Finish[...]
+ match = re.match(r"\w+\[(.*)\]\s*$", action_text, flags=re.DOTALL)
+ if match:
+ return match.group(1)
+
+ # 宽松格式:Finish: ... 或 Finish:...
+ m2 = re.match(r"finish\s*[::]\s*(.*)", action_text, flags=re.IGNORECASE | re.DOTALL)
+ if m2:
+ return m2.group(1)
+
+ # 再宽松:去掉前缀 "Finish" 后的剩余内容
+ if action_text.lower().startswith("finish"):
+ return action_text[len("finish"):].strip()
+
+ return ""
diff --git a/Co-creation-projects/aug618-Praxis/agents/reflection_agent.py b/Co-creation-projects/aug618-Praxis/agents/reflection_agent.py
new file mode 100644
index 00000000..35638d00
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/agents/reflection_agent.py
@@ -0,0 +1,180 @@
+"""Reflection Agent实现 - 自我反思与迭代优化的智能体"""
+
+from typing import Optional, List, Dict, Any
+from core.agent import Agent
+from core.llm import HelloAgentsLLM
+from core.config import Config
+from core.message import Message
+
+# 默认提示词模板
+DEFAULT_PROMPTS = {
+ "initial": """
+请根据以下要求完成任务:
+
+任务: {task}
+
+请提供一个完整、准确的回答。
+""",
+ "reflect": """
+请仔细审查以下回答,并找出可能的问题或改进空间:
+
+# 原始任务:
+{task}
+
+# 当前回答:
+{content}
+
+请分析这个回答的质量,指出不足之处,并提出具体的改进建议。
+如果回答已经很好,请回答"无需改进"。
+""",
+ "refine": """
+请根据反馈意见改进你的回答:
+
+# 原始任务:
+{task}
+
+# 上一轮回答:
+{last_attempt}
+
+# 反馈意见:
+{feedback}
+
+请提供一个改进后的回答。
+"""
+}
+
+class Memory:
+ """
+ 简单的短期记忆模块,用于存储智能体的行动与反思轨迹。
+ """
+ def __init__(self):
+ self.records: List[Dict[str, Any]] = []
+
+ def add_record(self, record_type: str, content: str):
+ """向记忆中添加一条新记录"""
+ self.records.append({"type": record_type, "content": content})
+ print(f"📝 记忆已更新,新增一条 '{record_type}' 记录。")
+
+ def get_trajectory(self) -> str:
+ """将所有记忆记录格式化为一个连贯的字符串文本"""
+ trajectory = ""
+ for record in self.records:
+ if record['type'] == 'execution':
+ trajectory += f"--- 上一轮尝试 (代码) ---\n{record['content']}\n\n"
+ elif record['type'] == 'reflection':
+ trajectory += f"--- 评审员反馈 ---\n{record['content']}\n\n"
+ return trajectory.strip()
+
+ def get_last_execution(self) -> str:
+ """获取最近一次的执行结果"""
+ for record in reversed(self.records):
+ if record['type'] == 'execution':
+ return record['content']
+ return ""
+
+class ReflectionAgent(Agent):
+ """
+ Reflection Agent - 自我反思与迭代优化的智能体
+
+ 这个Agent能够:
+ 1. 执行初始任务
+ 2. 对结果进行自我反思
+ 3. 根据反思结果进行优化
+ 4. 迭代改进直到满意
+
+ 特别适合代码生成、文档写作、分析报告等需要迭代优化的任务。
+
+ 支持多种专业领域的提示词模板,用户可以自定义或使用内置模板。
+ """
+
+ def __init__(
+ self,
+ name: str,
+ llm: HelloAgentsLLM,
+ system_prompt: Optional[str] = None,
+ config: Optional[Config] = None,
+ max_iterations: int = 3,
+ custom_prompts: Optional[Dict[str, str]] = None
+ ):
+ """
+ 初始化ReflectionAgent
+
+ Args:
+ name: Agent名称
+ llm: LLM实例
+ system_prompt: 系统提示词
+ config: 配置对象
+ max_iterations: 最大迭代次数
+ custom_prompts: 自定义提示词模板 {"initial": "", "reflect": "", "refine": ""}
+ """
+ super().__init__(name, llm, system_prompt, config)
+ self.max_iterations = max_iterations
+ self.memory = Memory()
+
+ # 设置提示词模板:用户自定义优先,否则使用默认模板
+ self.prompts = custom_prompts if custom_prompts else DEFAULT_PROMPTS
+
+ def run(self, input_text: str, **kwargs) -> str:
+ """
+ 运行Reflection Agent
+
+ Args:
+ input_text: 任务描述
+ **kwargs: 其他参数
+
+ Returns:
+ 最终优化后的结果
+ """
+ print(f"\n🤖 {self.name} 开始处理任务: {input_text}")
+
+ # 重置记忆
+ self.memory = Memory()
+
+ # 1. 初始执行
+ print("\n--- 正在进行初始尝试 ---")
+ initial_prompt = self.prompts["initial"].format(task=input_text)
+ initial_result = self._get_llm_response(initial_prompt, **kwargs)
+ self.memory.add_record("execution", initial_result)
+
+ # 2. 迭代循环:反思与优化
+ for i in range(self.max_iterations):
+ print(f"\n--- 第 {i+1}/{self.max_iterations} 轮迭代 ---")
+
+ # a. 反思
+ print("\n-> 正在进行反思...")
+ last_result = self.memory.get_last_execution()
+ reflect_prompt = self.prompts["reflect"].format(
+ task=input_text,
+ content=last_result
+ )
+ feedback = self._get_llm_response(reflect_prompt, **kwargs)
+ self.memory.add_record("reflection", feedback)
+
+ # b. 检查是否需要停止
+ if "无需改进" in feedback or "no need for improvement" in feedback.lower():
+ print("\n✅ 反思认为结果已无需改进,任务完成。")
+ break
+
+ # c. 优化
+ print("\n-> 正在进行优化...")
+ refine_prompt = self.prompts["refine"].format(
+ task=input_text,
+ last_attempt=last_result,
+ feedback=feedback
+ )
+ refined_result = self._get_llm_response(refine_prompt, **kwargs)
+ self.memory.add_record("execution", refined_result)
+
+ final_result = self.memory.get_last_execution()
+ print(f"\n--- 任务完成 ---\n最终结果:\n{final_result}")
+
+ # 保存到历史记录
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(final_result, "assistant"))
+
+ return final_result
+
+ def _get_llm_response(self, prompt: str, **kwargs) -> str:
+ """调用LLM并获取完整响应"""
+ messages = [{"role": "user", "content": prompt}]
+ return self.llm.invoke(messages, **kwargs) or ""
diff --git a/Co-creation-projects/aug618-Praxis/agents/simple_agent.py b/Co-creation-projects/aug618-Praxis/agents/simple_agent.py
new file mode 100644
index 00000000..d364f6e5
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/agents/simple_agent.py
@@ -0,0 +1,407 @@
+"""简单Agent实现 - 基于OpenAI原生API"""
+
+from typing import Optional, Iterator, TYPE_CHECKING, Callable
+import re
+
+from core.agent import Agent
+from core.llm import HelloAgentsLLM
+from core.config import Config
+from core.message import Message
+
+if TYPE_CHECKING:
+ from tools.registry import ToolRegistry
+
+class SimpleAgent(Agent):
+ """简单的对话Agent,支持可选的工具调用"""
+
+ def __init__(
+ self,
+ name: str,
+ llm: HelloAgentsLLM,
+ system_prompt: Optional[str] = None,
+ config: Optional[Config] = None,
+ tool_registry: Optional['ToolRegistry'] = None,
+ enable_tool_calling: bool = True,
+ tool_confirm_callback: Optional[Callable[[str, dict], bool]] = None,
+ ):
+ """
+ 初始化SimpleAgent
+
+ Args:
+ name: Agent名称
+ llm: LLM实例
+ system_prompt: 系统提示词
+ config: 配置对象
+ tool_registry: 工具注册表(可选,如果提供则启用工具调用)
+ enable_tool_calling: 是否启用工具调用(只有在提供tool_registry时生效)
+ """
+ super().__init__(name, llm, system_prompt, config)
+ self.tool_registry = tool_registry
+ self.enable_tool_calling = enable_tool_calling and tool_registry is not None
+ self.tool_confirm_callback = tool_confirm_callback
+
+ def _get_enhanced_system_prompt(self) -> str:
+ """构建增强的系统提示词,包含工具信息"""
+ base_prompt = self.system_prompt or "你是一个有用的AI助手。"
+
+ if not self.enable_tool_calling or not self.tool_registry:
+ return base_prompt
+
+ # 获取工具描述
+ tools_description = self.tool_registry.get_tools_description()
+ if not tools_description or tools_description == "暂无可用工具":
+ return base_prompt
+
+ tools_section = "\n\n## 可用工具\n"
+ tools_section += "你可以使用以下工具来帮助回答问题:\n"
+ tools_section += tools_description + "\n"
+
+ tools_section += "\n## 工具调用格式\n"
+ tools_section += "当需要使用工具时,请使用以下格式:\n"
+ tools_section += "`[TOOL_CALL:{tool_name}:{parameters}]`\n\n"
+
+ tools_section += "### 参数格式说明\n"
+ tools_section += "1. **多个参数**:使用 `key=value` 格式,用逗号分隔\n"
+ tools_section += " 示例:`[TOOL_CALL:calculator_multiply:a=12,b=8]`\n"
+ tools_section += " 示例:`[TOOL_CALL:filesystem_read_file:path=README.md]`\n\n"
+ tools_section += "2. **单个参数**:直接使用 `key=value`\n"
+ tools_section += " 示例:`[TOOL_CALL:search:query=Python编程]`\n\n"
+ tools_section += "3. **简单查询**:可以直接传入文本\n"
+ tools_section += " 示例:`[TOOL_CALL:search:Python编程]`\n\n"
+
+ tools_section += "### 重要提示\n"
+ tools_section += "- 参数名必须与工具定义的参数名完全匹配\n"
+ tools_section += "- 数字参数直接写数字,不需要引号:`a=12` 而不是 `a=\"12\"`\n"
+ tools_section += "- 文件路径等字符串参数直接写:`path=README.md`\n"
+ tools_section += "- 工具调用结果会自动插入到对话中,然后你可以基于结果继续回答\n"
+
+ return base_prompt + tools_section
+
+ def _parse_tool_calls(self, text: str) -> list:
+ """解析文本中的工具调用"""
+ pattern = r'\[TOOL_CALL:([^:]+):([^\]]+)\]'
+ matches = re.findall(pattern, text)
+
+ tool_calls = []
+ for tool_name, parameters in matches:
+ tool_calls.append({
+ 'tool_name': tool_name.strip(),
+ 'parameters': parameters.strip(),
+ 'original': f'[TOOL_CALL:{tool_name}:{parameters}]'
+ })
+
+ return tool_calls
+
+ def _execute_tool_call(self, tool_name: str, parameters: str) -> str:
+ """执行工具调用"""
+ if not self.tool_registry:
+ return f"❌ 错误:未配置工具注册表"
+
+ try:
+ # 获取Tool对象
+ tool = self.tool_registry.get_tool(tool_name)
+ if not tool:
+ return f"❌ 错误:未找到工具 '{tool_name}'"
+
+ # 智能参数解析
+ param_dict = self._parse_tool_parameters(tool_name, parameters)
+
+ # 交互式确认门(由上层执行器裁决是否允许执行)
+ if self.tool_confirm_callback is not None:
+ try:
+ allowed = bool(self.tool_confirm_callback(tool_name, param_dict))
+ except Exception as e:
+ return f"❌ 工具调用确认失败:{str(e)}"
+ if not allowed:
+ return "⛔️ 已取消本次工具调用(需要用户确认)。"
+
+ # 调用工具
+ result = tool.run(param_dict)
+ return f"🔧 工具 {tool_name} 执行结果:\n{result}"
+
+ except Exception as e:
+ return f"❌ 工具调用失败:{str(e)}"
+
+ def _parse_tool_parameters(self, tool_name: str, parameters: str) -> dict:
+ """智能解析工具参数"""
+ import json
+ param_dict = {}
+
+ # 尝试解析JSON格式
+ if parameters.strip().startswith('{'):
+ try:
+ param_dict = json.loads(parameters)
+ # JSON解析成功,进行类型转换
+ param_dict = self._convert_parameter_types(tool_name, param_dict)
+ return param_dict
+ except json.JSONDecodeError:
+ # JSON解析失败,继续使用其他方式
+ pass
+
+ if '=' in parameters:
+ # 格式: key=value 或 action=search,query=Python
+ if ',' in parameters:
+ # 多个参数:action=search,query=Python,limit=3
+ pairs = parameters.split(',')
+ for pair in pairs:
+ if '=' in pair:
+ key, value = pair.split('=', 1)
+ param_dict[key.strip()] = value.strip()
+ else:
+ # 单个参数:key=value
+ key, value = parameters.split('=', 1)
+ param_dict[key.strip()] = value.strip()
+
+ # 类型转换
+ param_dict = self._convert_parameter_types(tool_name, param_dict)
+
+ # 智能推断action(如果没有指定)
+ if 'action' not in param_dict:
+ param_dict = self._infer_action(tool_name, param_dict)
+ else:
+ # 直接传入参数,根据工具类型智能推断
+ param_dict = self._infer_simple_parameters(tool_name, parameters)
+
+ return param_dict
+
+ def _convert_parameter_types(self, tool_name: str, param_dict: dict) -> dict:
+ """
+ 根据工具的参数定义转换参数类型
+
+ Args:
+ tool_name: 工具名称
+ param_dict: 参数字典
+
+ Returns:
+ 类型转换后的参数字典
+ """
+ if not self.tool_registry:
+ return param_dict
+
+ tool = self.tool_registry.get_tool(tool_name)
+ if not tool:
+ return param_dict
+
+ # 获取工具的参数定义
+ try:
+ tool_params = tool.get_parameters()
+ except:
+ return param_dict
+
+ # 创建参数类型映射
+ param_types = {}
+ for param in tool_params:
+ param_types[param.name] = param.type
+
+ # 转换参数类型
+ converted_dict = {}
+ for key, value in param_dict.items():
+ if key in param_types:
+ param_type = param_types[key]
+ try:
+ if param_type == 'number' or param_type == 'integer':
+ # 转换为数字
+ if isinstance(value, str):
+ converted_dict[key] = float(value) if param_type == 'number' else int(value)
+ else:
+ converted_dict[key] = value
+ elif param_type == 'boolean':
+ # 转换为布尔值
+ if isinstance(value, str):
+ converted_dict[key] = value.lower() in ('true', '1', 'yes')
+ else:
+ converted_dict[key] = bool(value)
+ else:
+ converted_dict[key] = value
+ except (ValueError, TypeError):
+ # 转换失败,保持原值
+ converted_dict[key] = value
+ else:
+ converted_dict[key] = value
+
+ return converted_dict
+
+ def _infer_action(self, tool_name: str, param_dict: dict) -> dict:
+ """根据工具类型和参数推断action"""
+ if tool_name == 'memory':
+ if 'recall' in param_dict:
+ param_dict['action'] = 'search'
+ param_dict['query'] = param_dict.pop('recall')
+ elif 'store' in param_dict:
+ param_dict['action'] = 'add'
+ param_dict['content'] = param_dict.pop('store')
+ elif 'query' in param_dict:
+ param_dict['action'] = 'search'
+ elif 'content' in param_dict:
+ param_dict['action'] = 'add'
+ elif tool_name == 'rag':
+ if 'search' in param_dict:
+ param_dict['action'] = 'search'
+ param_dict['query'] = param_dict.pop('search')
+ elif 'query' in param_dict:
+ param_dict['action'] = 'search'
+ elif 'text' in param_dict:
+ param_dict['action'] = 'add_text'
+
+ return param_dict
+
+ def _infer_simple_parameters(self, tool_name: str, parameters: str) -> dict:
+ """为简单参数推断完整的参数字典"""
+ if tool_name == 'rag':
+ return {'action': 'search', 'query': parameters}
+ elif tool_name == 'memory':
+ return {'action': 'search', 'query': parameters}
+ else:
+ return {'input': parameters}
+
+ def run(self, input_text: str, max_tool_iterations: int = 3, **kwargs) -> str:
+ """
+ 运行SimpleAgent,支持可选的工具调用
+
+ Args:
+ input_text: 用户输入
+ max_tool_iterations: 最大工具调用迭代次数(仅在启用工具时有效)
+ **kwargs: 其他参数
+
+ Returns:
+ Agent响应
+ """
+ # 构建消息列表
+ messages = []
+
+ # 添加系统消息(可能包含工具信息)
+ enhanced_system_prompt = self._get_enhanced_system_prompt()
+ messages.append({"role": "system", "content": enhanced_system_prompt})
+
+ # 添加历史消息
+ for msg in self._history:
+ messages.append({"role": msg.role, "content": msg.content})
+
+ # 添加当前用户消息
+ messages.append({"role": "user", "content": input_text})
+
+ # 如果没有启用工具调用,使用原有逻辑
+ if not self.enable_tool_calling:
+ response = self.llm.invoke(messages, **kwargs)
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(response, "assistant"))
+ return response
+
+ # 迭代处理,支持多轮工具调用
+ current_iteration = 0
+ final_response = ""
+
+ while current_iteration < max_tool_iterations:
+ # 调用LLM
+ response = self.llm.invoke(messages, **kwargs)
+
+ # 检查是否有工具调用
+ tool_calls = self._parse_tool_calls(response)
+
+ if tool_calls:
+ # 执行所有工具调用并收集结果
+ tool_results = []
+ clean_response = response
+
+ for call in tool_calls:
+ result = self._execute_tool_call(call['tool_name'], call['parameters'])
+ tool_results.append(result)
+ # 从响应中移除工具调用标记
+ clean_response = clean_response.replace(call['original'], "")
+
+ # 构建包含工具结果的消息
+ messages.append({"role": "assistant", "content": clean_response})
+
+ # 添加工具结果
+ tool_results_text = "\n\n".join(tool_results)
+ messages.append({"role": "user", "content": f"工具执行结果:\n{tool_results_text}\n\n请基于这些结果给出完整的回答。"})
+
+ current_iteration += 1
+ continue
+
+ # 没有工具调用,这是最终回答
+ final_response = response
+ break
+
+ # 如果超过最大迭代次数,获取最后一次回答
+ if current_iteration >= max_tool_iterations and not final_response:
+ final_response = self.llm.invoke(messages, **kwargs)
+
+ # 保存到历史记录
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(final_response, "assistant"))
+
+ return final_response
+
+ def add_tool(self, tool) -> None:
+ """
+ 添加工具到Agent(便利方法)
+
+ 如果是MCP工具且启用了auto_expand,会自动展开为多个独立工具
+ """
+ if not self.tool_registry:
+ from tools.registry import ToolRegistry
+ self.tool_registry = ToolRegistry()
+ self.enable_tool_calling = True
+
+ # 检查是否是MCP工具且需要展开
+ if hasattr(tool, 'auto_expand') and tool.auto_expand:
+ # 获取展开的工具列表
+ expanded_tools = tool.get_expanded_tools()
+ if expanded_tools:
+ # 注册所有展开的工具
+ for expanded_tool in expanded_tools:
+ self.tool_registry.register_tool(expanded_tool)
+ print(f"✅ MCP工具 '{tool.name}' 已展开为 {len(expanded_tools)} 个独立工具")
+ return
+
+ # 普通工具或不展开的MCP工具
+ self.tool_registry.register_tool(tool)
+
+ def remove_tool(self, tool_name: str) -> bool:
+ """移除工具(便利方法)"""
+ if self.tool_registry:
+ return self.tool_registry.unregister_tool(tool_name)
+ return False
+
+ def list_tools(self) -> list:
+ """列出所有可用工具"""
+ if self.tool_registry:
+ return self.tool_registry.list_tools()
+ return []
+
+ def has_tools(self) -> bool:
+ """检查是否有可用工具"""
+ return self.enable_tool_calling and self.tool_registry is not None
+
+ def stream_run(self, input_text: str, **kwargs) -> Iterator[str]:
+ """
+ 流式运行Agent
+
+ Args:
+ input_text: 用户输入
+ **kwargs: 其他参数
+
+ Yields:
+ Agent响应片段
+ """
+ # 构建消息列表
+ messages = []
+
+ if self.system_prompt:
+ messages.append({"role": "system", "content": self.system_prompt})
+
+ for msg in self._history:
+ messages.append({"role": msg.role, "content": msg.content})
+
+ messages.append({"role": "user", "content": input_text})
+
+ # 流式调用LLM
+ full_response = ""
+ for chunk in self.llm.stream_invoke(messages, **kwargs):
+ full_response += chunk
+ yield chunk
+
+ # 保存完整对话到历史记录
+ self.add_message(Message(input_text, "user"))
+ self.add_message(Message(full_response, "assistant"))
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/__init__.py b/Co-creation-projects/aug618-Praxis/code_agent/__init__.py
new file mode 100644
index 00000000..f081dd14
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/__init__.py
@@ -0,0 +1,6 @@
+"""Code Agent implementations and CLI entrypoints.
+
+Note: `code_agent/langchain_agent.py` is kept as an optional demo/comparison implementation.
+The MVP CLI lives under `code_agent/cli/` and uses a HelloAgents-style hand-rolled executor + state machine.
+"""
+
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/agentic/__init__.py b/Co-creation-projects/aug618-Praxis/code_agent/agentic/__init__.py
new file mode 100644
index 00000000..452d994c
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/agentic/__init__.py
@@ -0,0 +1,5 @@
+"""Agentic Code Agent (ReAct + tools)"""
+
+from .code_agent import CodeAgent
+
+__all__ = ["CodeAgent"]
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/agentic/code_agent.py b/Co-creation-projects/aug618-Praxis/code_agent/agentic/code_agent.py
new file mode 100644
index 00000000..f3d3421a
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/agentic/code_agent.py
@@ -0,0 +1,540 @@
+from __future__ import annotations
+
+import json
+import os
+import shlex
+from dataclasses import dataclass
+from datetime import datetime
+from pathlib import Path
+from typing import Any, List, Optional
+
+from agents.react_agent import ReActAgent
+from core.config import Config
+from core.llm import HelloAgentsLLM
+from core.message import Message
+from context.builder import ContextBuilder, ContextConfig, ContextPacket
+from tools.registry import ToolRegistry
+from tools.builtin.note_tool import NoteTool
+from tools.builtin.terminal_tool import TerminalTool
+from tools.builtin.plan_tool import PlanTool
+from tools.builtin.todo_tool import TodoTool
+from tools.builtin.context_fetch_tool import ContextFetchTool
+from tools.builtin.protocol_tools import MCPTool
+from tools.builtin.skills_tool import SkillsTool
+from utils.env import env_flag, env_flag_true, env_stripped
+from utils.multimodal import image_part_from_path
+from utils.references import parse_references
+from tools.builtin.ocr_tool import extract_text_from_image
+
+
+
+@dataclass
+class CodeAgentPaths:
+ """CodeAgent 路径配置类,集中管理所有相关目录路径"""
+ repo_root: Path
+ notes_dir: Path
+ memory_dir: Path
+ sessions_dir: Path
+ logs_dir: Path
+
+ @property
+ def helloagents_dir(self) -> Path:
+ """返回 .helloagents 目录路径"""
+ return self.repo_root / ".helloagents"
+
+ @property
+ def prompts_dir(self) -> Path:
+ """返回 prompts 目录路径"""
+ return self.repo_root / "code_agent" / "prompts"
+
+
+class CodeAgent:
+ """
+ 类似 Claude Code/Codex 的 CLI 智能体:
+ - 核心循环使用 ReActAgent。
+ - ContextBuilder 负责拼接:系统提示词 + 最近对话 + 相关笔记 + 情景记忆。
+ - 规划能力作为可选工具 (`plan`) 暴露给模型,模型可按需调用。
+ """
+
+ def __init__(self, repo_root: Path, llm: Optional[HelloAgentsLLM] = None, config: Optional[Config] = None):
+ """
+ 初始化 CodeAgent
+
+ Args:
+ repo_root: 代码仓库根目录
+ llm: LLM 实例
+ config: 配置对象
+ """
+ repo_root = repo_root.resolve()
+ self.config = config or Config.from_env()
+
+ # 初始化目录结构
+ helloagents_dir = Path(self.config.helloagents_dir)
+ state_root = helloagents_dir if helloagents_dir.is_absolute() else (repo_root / helloagents_dir)
+ self.paths = CodeAgentPaths(
+ repo_root=repo_root,
+ notes_dir=state_root / "notes",
+ memory_dir=state_root / "memory",
+ sessions_dir=state_root / "sessions",
+ logs_dir=state_root / "logs",
+ )
+ # 确保所有必要目录存在
+ self.paths.helloagents_dir.mkdir(parents=True, exist_ok=True)
+ self.paths.notes_dir.mkdir(parents=True, exist_ok=True)
+ self.paths.sessions_dir.mkdir(parents=True, exist_ok=True)
+ self.paths.logs_dir.mkdir(parents=True, exist_ok=True)
+ os.environ.setdefault("CODE_AGENT_LOG_DIR", str(self.paths.logs_dir))
+ # memory / logs 仅在需要时创建,这里不再预建
+
+ self.session_id = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+ self.llm = llm or HelloAgentsLLM()
+ self._quiet = env_flag("CODE_AGENT_QUIET", default=False)
+
+ # 初始化工具 (真实实现)
+ self.note_tool = NoteTool(workspace=str(self.paths.notes_dir))
+ # 类似 Claude Code:默认允许 Shell 语法 (管道等),但危险操作需确认
+ self.terminal_tool = TerminalTool(
+ workspace=str(self.paths.repo_root),
+ timeout=60,
+ confirm_dangerous=True,
+ default_shell_mode=True,
+ )
+ self.todo_tool = TodoTool(workspace=str(self.paths.helloagents_dir / "todos"))
+
+ # ReActAgent 的工具注册表
+ # 核心工具:terminal, note, memory, plan
+ # 扩展上下文工具:context_fetch(让模型按需获取更多证据)
+ self.registry = ToolRegistry()
+ self.registry.register_tool(self.terminal_tool)
+ self.registry.register_tool(self.note_tool)
+ self.registry.register_tool(PlanTool(self.llm, prompt_path=str(self.paths.prompts_dir / "plan.md")))
+ self.registry.register_tool(self.todo_tool)
+
+ # 注册上下文获取工具(让模型按需探索)
+ self.context_fetch_tool = ContextFetchTool(
+ workspace=str(self.paths.repo_root),
+ note_tool=self.note_tool,
+ memory_tool=None,
+ max_tokens_per_source=800,
+ context_lines=5,
+ )
+ self.registry.register_tool(self.context_fetch_tool)
+
+ # ========== Skills(.agents/skills)==========
+ # 用于渐进式披露的 SOP/工作流;存在则自动注册
+ try:
+ # SkillsTool 会按 OpenCode/Claude 的标准路径扫描,
+ # 也可用 CODE_AGENT_SKILLS_DIR 覆盖为单一根目录。
+ self.registry.register_tool(SkillsTool(repo_root=str(self.paths.repo_root)))
+ except Exception:
+ pass
+
+ # ========== MCP Monitor 工具(系统监控)==========
+ # 通过环境变量 MCP_MONITOR_COMMAND 指定启动命令。
+ monitor_cmd: Optional[List[str]] = None
+ env_cmd = env_stripped("MCP_MONITOR_COMMAND", "")
+ if env_cmd:
+ monitor_cmd = shlex.split(env_cmd)
+
+ if monitor_cmd:
+ try:
+ monitor_tool = MCPTool(
+ name="monitor",
+ server_command=monitor_cmd,
+ auto_expand=True,
+ )
+ for t in monitor_tool.get_expanded_tools():
+ self.registry.register_tool(t)
+ if not self._quiet:
+ print(f"✅ MCP Monitor 已注册({len(monitor_tool.get_expanded_tools())} 工具)")
+ except Exception as e:
+ if not self._quiet:
+ print(f"⚠️ MCP Monitor 注册失败: {e}")
+
+ # ========== MCP Playwright 工具(网页自动化)==========
+ # 通过环境变量 MCP_PLAYWRIGHT_COMMAND 指定启动命令
+ playwright_cmd: Optional[List[str]] = None
+ env_playwright = env_stripped("MCP_PLAYWRIGHT_COMMAND", "")
+ if env_playwright:
+ playwright_cmd = shlex.split(env_playwright)
+
+ if playwright_cmd:
+ try:
+ playwright_tool = MCPTool(
+ name="playwright",
+ server_command=playwright_cmd,
+ auto_expand=True,
+ )
+ for t in playwright_tool.get_expanded_tools():
+ self.registry.register_tool(t)
+ if not self._quiet:
+ print(f"✅ MCP Playwright 已注册({len(playwright_tool.get_expanded_tools())} 工具)")
+ except Exception as e:
+ if not self._quiet:
+ print(f"⚠️ MCP Playwright 注册失败: {e}")
+
+ # 初始化上下文构建器(lazy_fetch=True:只构建保底上下文)
+ self.context_builder = ContextBuilder(
+ memory_tool=None,
+ rag_tool=None,
+ config=ContextConfig(
+ max_tokens=8000,
+ reserve_ratio=0.15,
+ max_history_turns=10,
+ enable_compression=True,
+ include_output_format=False,
+ lazy_fetch=True, # 按需探索模式
+ ),
+ llm=self.llm,
+ )
+
+ # 加载自定义 Prompt 并初始化 ReActAgent
+ react_prompt = (self.paths.prompts_dir / "react.md").read_text(encoding="utf-8")
+ summarize_prompt = (self.paths.prompts_dir / "summarize_observation.md").read_text(encoding="utf-8")
+
+ def _summarize_observation(tool_name: str, tool_input: str, observation: str) -> str:
+ """
+ 使用 LLM 压缩工具输出 (避免将巨大的原始输出放入 Prompt)
+ """
+ truncated = observation
+ if len(truncated) > 8000:
+ truncated = truncated[:8000] + "\n...truncated...\n"
+ user_msg = (
+ f"Tool: {tool_name}\n"
+ f"Input: {tool_input}\n\n"
+ f"Output:\n{truncated}"
+ )
+ return self.llm.invoke(
+ [
+ {"role": "system", "content": summarize_prompt},
+ {"role": "user", "content": user_msg},
+ ],
+ max_tokens=400,
+ ) or ""
+
+ self.react = ReActAgent(
+ name="code_agent",
+ llm=self.llm,
+ tool_registry=self.registry,
+ max_steps=20,
+ custom_prompt=react_prompt,
+ observation_summarizer=_summarize_observation,
+ summarize_threshold_chars=2500,
+ )
+
+ base_system = (self.paths.prompts_dir / "system.md").read_text(encoding="utf-8")
+ self.tools_reference_path = self.paths.prompts_dir / "tools.md"
+ self.system_prompt = base_system
+ self.history: List[Message] = []
+ self.recent_tool_packets: List[ContextPacket] = []
+ self.last_direct_reply: bool = False
+
+ def _is_chitchat(self, text: str) -> bool:
+ """判断是否为闲聊,避免不必要的工具调用"""
+ t = (text or "").strip().lower()
+ return t in {"hi", "hello", "hey", "yo", "你好", "您好", "在吗", "嗨", "哈喽"}
+
+ def _is_history_query(self, text: str) -> bool:
+ """判断是否为'回顾刚才说了什么'的元请求"""
+ t = (text or "").strip().lower()
+ patterns = [
+ "说了什么",
+ "刚才说了什么",
+ "之前说了什么",
+ "what did i say",
+ "what did we say",
+ "recap",
+ "summary of conversation",
+ ]
+ return any(p in t for p in patterns)
+
+ def _reply_with_recent_history(self, limit: int = 6) -> str:
+ """生成最近对话的简要回顾"""
+ # 只取用户/助手消息(跳过系统等)
+ items = [m for m in self.history if m.role in {"user", "assistant"}][-limit * 2 :]
+ if not items:
+ return "目前还没有可回顾的对话历史。"
+ lines = []
+ for m in items:
+ role = "你" if m.role == "user" else "助手"
+ lines.append(f"- {role}: {m.content}")
+ return "下面是最近的对话回顾:\n" + "\n".join(lines)
+
+ # 以下两个方法在 lazy_fetch 模式下不再主动调用,
+ # 扩展上下文改由模型通过 context_fetch 工具按需获取。
+ # 保留这些方法以支持 lazy_fetch=False 的传统模式。
+
+ def _note_packets(self, query: str) -> List[ContextPacket]:
+ """检索相关笔记并封装为 ContextPacket"""
+ packets: List[ContextPacket] = []
+ if self._is_chitchat(query):
+ return packets
+ try:
+ # 获取最近的阻碍 (Blocker)
+ blockers = self.note_tool.run({"action": "list", "note_type": "blocker", "limit": 2})
+ if blockers and isinstance(blockers, str) and "暂无" not in blockers:
+ packets.append(ContextPacket(content=f"[Notes:blocker]\n{blockers}", metadata={"source": "note"}))
+ # 搜索相关笔记
+ hits = self.note_tool.run({"action": "search", "query": query, "limit": 3})
+ if hits and isinstance(hits, str) and "未找到" not in hits:
+ packets.append(ContextPacket(content=f"[Notes:search]\n{hits}", metadata={"source": "note"}))
+ except Exception:
+ pass
+ return packets
+
+ def _memory_packets(self, query: str) -> List[ContextPacket]:
+ """检索相关记忆并封装为 ContextPacket"""
+ packets: List[ContextPacket] = []
+ if self._is_chitchat(query):
+ return packets
+ try:
+ hits = self.memory_tool.run(
+ {"action": "search", "query": query, "memory_types": self.memory_tool.memory_types, "limit": 5, "min_importance": 0.0}
+ )
+ if hits and isinstance(hits, str) and "未找到" not in hits:
+ packets.append(ContextPacket(content=f"[Memory]\n{hits}", metadata={"source": "memory"}))
+ except Exception:
+ pass
+ return packets
+
+ def _persist_session(self) -> None:
+ """持久化当前会话到 JSON 文件"""
+ p = self.paths.sessions_dir / f"{self.session_id}.json"
+ data = {
+ "session_id": self.session_id,
+ "updated_at": datetime.now().isoformat(),
+ "history": [
+ {"role": m.role, "content": m.content, "timestamp": m.timestamp.isoformat()} for m in self.history[-50:]
+ ],
+ }
+ p.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
+
+ def _capture_recent_tool_evidence(self) -> tuple[bool, bool]:
+ """收集最近一轮 ReAct 工具摘要,并返回 todo 使用情况。"""
+ todo_used = False
+ todo_listed = False
+ tool_summaries: List[str] = []
+
+ try:
+ for item in getattr(self.react, "last_trace", [])[-6:]:
+ summary = item.get("observation_summary")
+ tool_name = item.get("tool_name")
+ if tool_name == "todo":
+ todo_used = True
+ if "list" in str(item.get("tool_input", "")):
+ todo_listed = True
+ if summary:
+ tool_summaries.append(
+ f"[{tool_name}] {item.get('tool_input')}\n{summary}"
+ )
+
+ if tool_summaries:
+ self.recent_tool_packets.append(
+ ContextPacket(
+ content="[Tool Evidence]\n" + "\n\n".join(tool_summaries),
+ metadata={"type": "tool_result", "source": "react"},
+ )
+ )
+ if len(self.recent_tool_packets) > 8:
+ self.recent_tool_packets = self.recent_tool_packets[-8:]
+ except Exception:
+ pass
+
+ return todo_used, todo_listed
+
+ def _append_todo_snapshot_if_needed(self, response: str, todo_used: bool, todo_listed: bool) -> str:
+ """如果本轮使用过 todo 但未主动 list,则在回复末尾补充看板快照。"""
+ if not todo_used or todo_listed:
+ return response
+
+ try:
+ todo_snapshot = self.todo_tool.run({"action": "list"})
+ return f"{response}\n\nTodo board:\n{todo_snapshot}"
+ except Exception:
+ return response
+
+ def run_turn(self, user_input: str, image_paths: Optional[List[str | Path]] = None) -> str:
+ """
+ 执行一轮对话:
+ 1. 解析 @file/@dir 引用
+ 2. 收集上下文 (笔记、记忆、最近工具输出)
+ 3. 构建完整 Prompt
+ 4. 运行 ReAct 循环
+ 5. 更新历史并持久化
+ """
+ # 空输入:提示而不进入 ReAct
+ if not user_input.strip():
+ return "请提供具体指令或问题。"
+
+ # ========== 解析 @file/@dir 引用 ==========
+ refs = parse_references(user_input, workspace=self.paths.repo_root)
+
+ # 如果有解析错误,先报告
+ if refs.errors:
+ error_msg = "引用解析警告:\n" + "\n".join(f"- {e}" for e in refs.errors)
+ print(error_msg)
+
+ # 使用清理后的 query(移除了 @file/@dir 标记)
+ clean_query = refs.clean_query
+
+ # ========== 根据模型类型处理图片 ==========
+ # 多模态模型 → 图片作为 attachments 直接发送
+ # 文本模型 → 图片走 OCR,提取的文字注入到 context
+ all_attachments: List[dict[str, Any]] = []
+ ocr_results: List[str] = []
+
+ # 收集所有图片路径
+ all_image_paths: List[Path] = list(refs.image_paths)
+ if image_paths:
+ all_image_paths.extend([Path(p) for p in image_paths])
+
+ if self.llm.is_multimodal:
+ # 多模态模型:图片作为 attachments
+ all_attachments = list(refs.image_attachments)
+ if image_paths:
+ for p in image_paths:
+ all_attachments.append(image_part_from_path(p))
+ if all_image_paths:
+ print(f"📷 多模态模式:{len(all_image_paths)} 张图片将直接发送给 LLM")
+ else:
+ # 文本模型:图片走 OCR
+ if all_image_paths:
+ print(f"🔍 文本模式:{len(all_image_paths)} 张图片将通过 OCR 提取文字")
+ for img_path in all_image_paths:
+ ocr_text = extract_text_from_image(img_path)
+ if ocr_text and not ocr_text.startswith("错误") and not ocr_text.startswith("OCR 失败"):
+ ocr_results.append(f"[OCR 识别: {img_path.name}]\n```\n{ocr_text}\n```")
+ print(f" ✓ {img_path.name}: 提取到 {len(ocr_text)} 字符")
+ else:
+ ocr_results.append(f"[OCR: {img_path.name}] ⚠️ {ocr_text}")
+ print(f" ✗ {img_path.name}: {ocr_text[:50]}...")
+
+ # 移除 context_blocks 中的图片占位符(因为已经用 OCR 结果替代)
+ refs.context_blocks = [b for b in refs.context_blocks if not b.startswith("[图片:")]
+
+ # 闲聊/问候:直接回复,避免 ReAct 的严格格式解析失败,也避免无谓的工具调用。
+ if self._is_chitchat(clean_query) and not refs.context_blocks and not all_attachments:
+ self.last_direct_reply = True
+ reply = "你好!我是 奶龙版Code Agent,可以帮你按需探索代码仓库、生成补丁并在确认后落盘。你想做什么?(例如:分析项目结构 / 搜索某个类 / 修复一个报错)"
+ self.history.append(Message(content=user_input, role="user", timestamp=datetime.now()))
+ self.history.append(Message(content=reply, role="assistant", timestamp=datetime.now()))
+ if len(self.history) > 50:
+ self.history = self.history[-50:]
+ self._persist_session()
+ return reply
+ self.last_direct_reply = False
+
+ # 元请求:回顾最近对话
+ if self._is_history_query(clean_query) and not refs.context_blocks:
+ self.last_direct_reply = True
+ reply = self._reply_with_recent_history(limit=6)
+ self.history.append(Message(content=user_input, role="user", timestamp=datetime.now()))
+ self.history.append(Message(content=reply, role="assistant", timestamp=datetime.now()))
+ if len(self.history) > 50:
+ self.history = self.history[-50:]
+ self._persist_session()
+ return reply
+
+ # 若检测到明显多步骤词汇,向模型追加轻量提示(不强制,只提高倾向)
+ multistep_hint = ""
+ multi_patterns = ["分步", "步骤", "三步", "计划", "改造", "完成后", "多步", "多步骤"]
+ if any(p in clean_query for p in multi_patterns):
+ multistep_hint = "提示:本任务包含多个步骤,先用 todo 记录/更新,再执行;收尾用 todo list 汇总。"
+
+ # Skills 索引(渐进式披露的“目录”):让模型知道本地有哪些 skills,
+ # 从而能在合适时机调用 skills[show] 读取 SKILL.md 的 SOP。
+ skills_index = ""
+ auto_skill_sop = ""
+ try:
+ if env_flag_true("CODE_AGENT_ENABLE_SKILLS_INDEX", default=True):
+ skills_tool = self.registry.get_tool("skills")
+ if skills_tool is not None:
+ # 只注入轻量索引(name/description),不加载全文
+ skills_index = skills_tool.run({"action": "list", "limit": 20})
+ if skills_index and "未找到 skills" not in skills_index:
+ skills_index = "\n\n[Available Skills]\n" + skills_index + "\n\n用法:先 skills[search] 再 skills[show] 加载具体 SOP。"
+
+ # 外部 skills 发现/安装:用户已经明确表达“去外部找/装 skills”时,
+ # 直接加载 find-skills 的 SOP(仍是按需加载,只对该意图触发)。
+ if env_flag_true("CODE_AGENT_AUTO_LOAD_FIND_SKILLS", default=True):
+ ql = clean_query.lower()
+ external_intent = any(
+ k in ql
+ for k in [
+ "外部",
+ "生态",
+ "安装skill",
+ "安装 skills",
+ "安装 skill",
+ "找skill",
+ "找 skills",
+ "找 skill",
+ "搜索skill",
+ "搜索 skills",
+ "npx skills",
+ "skills add",
+ "skills find",
+ ]
+ )
+ if external_intent:
+ try:
+ sop = skills_tool.run({"action": "show", "id": "find-skills"})
+ if sop and "未找到 skill" not in sop:
+ auto_skill_sop = (
+ "\n\n[Auto-loaded Skill SOP: find-skills]\n"
+ + sop
+ + "\n\n要求:当用户要在外部生态查找/安装技能时,严格按上面的 SOP 执行。"
+ )
+ except Exception:
+ auto_skill_sop = ""
+ except Exception:
+ skills_index = ""
+ auto_skill_sop = ""
+
+ # 构建保底上下文(系统提示 + 对话历史 + 上次工具摘要 + 可选 hint)
+ # 扩展上下文由模型通过 context_fetch 工具按需获取
+ tool_summaries = []
+ for packet in self.recent_tool_packets[-3:]:
+ tool_summaries.append(packet.content)
+
+ # 如果有 @file/@dir 引用的内容或 OCR 结果,作为额外上下文注入
+ ref_context = ""
+ all_context_parts = []
+ if refs.context_blocks:
+ all_context_parts.extend(refs.context_blocks)
+ if ocr_results:
+ all_context_parts.extend(ocr_results)
+ if all_context_parts:
+ ref_context = "\n\n[用户引用的文件/目录]\n" + "\n\n".join(all_context_parts)
+
+ context_text = self.context_builder.build_base(
+ user_query=clean_query + ref_context,
+ conversation_history=self.history,
+ system_instructions=self.system_prompt
+ + (("\n" + multistep_hint) if multistep_hint else "")
+ + (skills_index or "")
+ + (auto_skill_sop or ""),
+ tool_summaries=tool_summaries if tool_summaries else None,
+ )
+
+ # 准备多模态附件
+ attachments: Optional[List[dict[str, Any]]] = all_attachments if all_attachments else None
+
+ # 将拼接好的上下文作为"问题"输入给 ReAct
+ response = self.react.run(context_text, max_tokens=8000, attachments=attachments)
+
+ # 收集本轮的工具执行证据 (已在 ReActAgent 内部摘要)
+ todo_used, todo_listed = self._capture_recent_tool_evidence()
+
+ # 更新历史记录 (保留最近 50 条)
+ # 记录原始输入(包含 @file/@dir 标记,便于回顾)
+ user_for_history = user_input
+ if image_paths:
+ rendered = "\n".join([f"- {str(Path(p))}" for p in image_paths])
+ user_for_history = f"{user_input}\n\n[附加图片]\n{rendered}"
+ self.history.append(Message(content=user_for_history, role="user", timestamp=datetime.now()))
+ self.history.append(Message(content=response, role="assistant", timestamp=datetime.now()))
+ if len(self.history) > 50:
+ self.history = self.history[-50:]
+ self._persist_session()
+ return self._append_todo_snapshot_if_needed(response, todo_used, todo_listed)
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/executors/__init__.py b/Co-creation-projects/aug618-Praxis/code_agent/executors/__init__.py
new file mode 100644
index 00000000..6ccf66f9
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/executors/__init__.py
@@ -0,0 +1,2 @@
+"""Executors are the only place with side effects (writes)."""
+
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/executors/apply_patch_executor.py b/Co-creation-projects/aug618-Praxis/code_agent/executors/apply_patch_executor.py
new file mode 100644
index 00000000..07ac08d4
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/executors/apply_patch_executor.py
@@ -0,0 +1,512 @@
+from __future__ import annotations
+
+import os
+import tempfile
+from dataclasses import dataclass
+from datetime import datetime
+from pathlib import Path
+from typing import List, Optional, Tuple
+
+
+class PatchApplyError(RuntimeError):
+ """
+ 补丁应用过程中发生的异常类。
+ 用于封装补丁应用失败的原因,并提供额外的检查目标信息。
+
+ 参数:
+ message: 错误消息,描述补丁应用失败的原因
+ recheck_targets: 可选的重新检查目标列表,用于辅助调试和修复补丁问题
+ """
+ def __init__(self, message: str, recheck_targets: Optional[List[str]] = None):
+ super().__init__(message)
+ self.recheck_targets = recheck_targets or []
+
+
+@dataclass
+class ApplyResult:
+ """
+ 补丁应用结果的数据类。
+ 用于返回补丁应用过程中产生的变更信息和备份信息。
+
+ 字段:
+ files_changed: 被修改的文件路径列表(相对路径)
+ backups: 创建的备份文件路径列表(绝对路径)
+ """
+ files_changed: List[str]
+ backups: List[str]
+
+
+class ApplyPatchExecutor:
+ """
+ 应用 Codex 风格的 *** Begin Patch 格式补丁。
+
+ 安全特性 (MVP):
+ - repo_root 路径限制 (防止路径逃逸)
+ - 通过临时文件 + os.replace 实现原子写入
+ - 备份到 /.helloagents/backups//
+ - 大小限制 (最大文件数, 最大总变更行数)
+ - Update File 块的冲突检测 (精确匹配)
+ """
+
+ def __init__(
+ self,
+ repo_root: Path,
+ max_files: int = 10,
+ max_total_changed_lines: int = 800,
+ allowed_write_suffixes: Optional[List[str]] = None,
+ ):
+ """
+ 初始化补丁应用执行器。
+
+ 参数:
+ repo_root: 代码仓库根目录路径,所有补丁操作都限制在此目录内
+ max_files: 单个补丁允许修改的最大文件数量,默认10个
+ max_total_changed_lines: 单个补丁允许修改的最大总行数,默认800行
+ allowed_write_suffixes: 允许修改的文件后缀列表,默认只允许常见文本文件
+ """
+ self.repo_root = repo_root
+ self.max_files = max_files
+ self.max_total_changed_lines = max_total_changed_lines
+
+ # 默认允许写入的文件后缀,防止意外修改二进制文件或敏感文件
+ self.allowed_write_suffixes = allowed_write_suffixes or [
+ ".py",
+ ".md",
+ ".toml",
+ ".json",
+ ".yml",
+ ".yaml",
+ ".txt",
+ ".html",
+ ".htm",
+ ".css",
+ ".js",
+ ]
+
+ # 初始化工作目录和备份目录
+ self.root_dir = repo_root / ".helloagents"
+ self.backups_dir = self.root_dir / "backups"
+ self.backups_dir.mkdir(parents=True, exist_ok=True)
+
+ def apply(self, patch_text: str) -> ApplyResult:
+ """
+ 解析并应用补丁文本。
+
+ 执行流程:
+ 1. 解析补丁操作 (Add/Update/Delete)
+ 2. 检查安全限制 (文件数量, 变更行数)
+ 3. 创建备份目录
+ 4. 逐个执行操作 (先备份再修改)
+
+ 参数:
+ patch_text: 符合 Codex 风格的补丁文本,以 *** Begin Patch 开始,*** End Patch 结束
+
+ 返回:
+ ApplyResult: 包含被修改文件和备份文件信息的结果对象
+
+ 异常:
+ PatchApplyError: 当补丁不符合格式、超出限制或应用失败时抛出
+ """
+ # 解析补丁文本,提取操作列表
+ ops = self._parse_patch(patch_text)
+
+ # 统计受影响的文件数量,检查是否超过限制
+ touched_files = [op[1] for op in ops if op[0] in {"add", "update", "delete"}]
+ if len(set(touched_files)) > self.max_files:
+ raise PatchApplyError(f"Too many files in patch: {len(set(touched_files))} > {self.max_files}")
+
+ # 估算补丁修改的总行数,检查是否超过限制
+ total_changed = self._estimate_changed_lines(ops)
+ if total_changed > self.max_total_changed_lines:
+ raise PatchApplyError(f"Patch too large: {total_changed} changed lines > {self.max_total_changed_lines}")
+
+ # 创建本次补丁应用的专属备份目录(时间戳命名)
+ backup_run_dir = self.backups_dir / datetime.now().strftime("%Y%m%d_%H%M%S")
+ backup_run_dir.mkdir(parents=True, exist_ok=True)
+
+ # 初始化结果收集变量
+ files_changed: List[str] = [] # 记录被修改的文件路径
+ backups: List[str] = [] # 记录创建的备份文件路径
+
+ # 遍历所有解析出的操作,逐个执行
+ for kind, rel_path, payload in ops:
+ # 安全检查:确保路径在仓库内,防止路径遍历攻击
+ target = self._safe_path(rel_path)
+
+ # 安全检查:确保文件后缀在允许的列表中
+ self._enforce_suffix(target)
+
+ if kind == "add":
+ # 添加新文件操作
+ if target.exists():
+ raise PatchApplyError(f"Add File target already exists: {rel_path}")
+ # 创建父目录(如果不存在)
+ target.parent.mkdir(parents=True, exist_ok=True)
+ # 原子写入新文件内容
+ self._atomic_write(target, payload)
+ # 记录变更
+ files_changed.append(rel_path)
+
+ elif kind == "delete":
+ # 删除文件操作
+ if not target.exists():
+ raise PatchApplyError(f"Delete File target missing: {rel_path}")
+ # 删除前先备份文件
+ b = self._backup_file(target, backup_run_dir)
+ backups.append(str(b))
+ # 删除文件
+ target.unlink()
+ # 记录变更
+ files_changed.append(rel_path)
+
+ elif kind == "update":
+ # 更新文件操作
+ if not target.exists():
+ raise PatchApplyError(f"Update File target missing: {rel_path}")
+ # 读取原始文件内容(保留换行符)
+ original = target.read_text(encoding="utf-8").splitlines(keepends=True)
+ # 修改前先备份文件
+ b = self._backup_file(target, backup_run_dir)
+ backups.append(str(b))
+ # 应用更新补丁内容
+ updated = self._apply_update_payload(original, payload, rel_path)
+ # 原子写入更新后的内容
+ self._atomic_write(target, "".join(updated))
+ # 记录变更
+ files_changed.append(rel_path)
+
+ else:
+ # 未知操作类型
+ raise PatchApplyError(f"Unknown op kind: {kind}")
+
+ # 返回最终的应用结果
+ return ApplyResult(files_changed=files_changed, backups=backups)
+
+ def _safe_path(self, rel_path: str) -> Path:
+ """
+ 验证路径安全性,防止路径遍历攻击 (Path Traversal)。
+ 确保目标路径在 repo_root 目录下,防止访问仓库外的文件。
+
+ 参数:
+ rel_path: 相对路径字符串
+
+ 返回:
+ Path: 安全的绝对路径对象
+
+ 异常:
+ PatchApplyError: 当路径是绝对路径、包含特殊字符或试图访问仓库外时抛出
+ """
+ if rel_path.startswith("/") or rel_path.startswith("~"):
+ raise PatchApplyError(f"Absolute paths are not allowed: {rel_path}")
+ target = (self.repo_root / rel_path).resolve()
+ # 检查解析后的路径是否以 repo_root 开头
+ if not str(target).startswith(str(self.repo_root.resolve()) + os.sep) and target != self.repo_root.resolve():
+ raise PatchApplyError(f"Path escapes repo_root: {rel_path}")
+ if target.exists() and target.is_symlink():
+ raise PatchApplyError(f"Refusing to modify symlink: {rel_path}")
+ return target
+
+ def _enforce_suffix(self, target: Path) -> None:
+ """
+ 检查目标文件的后缀是否在允许的列表中。
+ 防止意外修改二进制文件、配置文件或其他敏感文件。
+
+ 参数:
+ target: 目标文件路径对象
+
+ 异常:
+ PatchApplyError: 当文件后缀不在允许列表中时抛出
+ """
+ if target.suffix and target.suffix not in self.allowed_write_suffixes:
+ raise PatchApplyError(f"Disallowed file suffix for write: {target.suffix}")
+
+ def _backup_file(self, target: Path, backup_run_dir: Path) -> Path:
+ """
+ 备份目标文件到指定的备份目录。
+ 备份文件保持与原文件相同的相对路径结构,后缀添加 .bak。
+
+ 参数:
+ target: 要备份的目标文件路径
+ backup_run_dir: 本次运行的备份目录
+
+ 返回:
+ Path: 创建的备份文件路径
+ """
+ # 获取文件相对于仓库根目录的路径
+ rel = target.relative_to(self.repo_root)
+ # 构建备份文件路径
+ backup_path = backup_run_dir / (str(rel) + ".bak")
+ # 创建备份文件的父目录(如果不存在)
+ backup_path.parent.mkdir(parents=True, exist_ok=True)
+ # 复制文件内容到备份文件
+ backup_path.write_bytes(target.read_bytes())
+ return backup_path
+
+ def _atomic_write(self, target: Path, content: str) -> None:
+ """
+ 原子写入文件内容。
+ 先写入临时文件,然后使用 os.replace 原子性替换目标文件,确保写入过程不会因为中断而导致文件损坏。
+
+ 参数:
+ target: 目标文件路径
+ content: 要写入的文件内容
+ """
+ target.parent.mkdir(parents=True, exist_ok=True)
+ with tempfile.NamedTemporaryFile("w", delete=False, dir=str(target.parent), encoding="utf-8") as tf:
+ tf.write(content)
+ tf.flush()
+ os.fsync(tf.fileno())
+ tmp_name = tf.name
+ os.replace(tmp_name, target)
+
+ def _parse_patch(self, text: str) -> List[Tuple[str, str, str]]:
+ """
+ 解析补丁文本,提取操作列表。
+ 支持的操作:
+ - *** Add File: - 添加新文件
+ - *** Delete File: - 删除文件
+ - *** Update File: - 更新文件内容
+
+ 参数:
+ text: 补丁文本字符串
+
+ 返回:
+ List[Tuple[str, str, str]]: 操作列表,每个操作包含(操作类型, 路径, 内容)
+
+ 异常:
+ PatchApplyError: 当补丁格式不符合要求时抛出
+ """
+ lines = text.splitlines()
+ # 宽容处理:跳过前置空行/代码块围栏,找到真正的开头
+ while lines and lines[0].strip() in {"", "```", "```patch", "```diff", "```text"}:
+ lines = lines[1:]
+ # 如果仍未以标头开头,尝试向下寻找标头并截取
+ if lines and lines[0].strip() != "*** Begin Patch":
+ for idx, l in enumerate(lines):
+ if l.strip() == "*** Begin Patch":
+ lines = lines[idx:]
+ break
+ if not lines or lines[0].strip() != "*** Begin Patch":
+ raise PatchApplyError("Patch must start with '*** Begin Patch'")
+ # 同样跳过结尾的围栏/空行
+ while lines and lines[-1].strip() in {"", "```"}:
+ lines = lines[:-1]
+ if not lines or lines[-1].strip() != "*** End Patch":
+ # 如果末尾未对齐,尝试在中间找到最后一个 End 标记
+ for idx in range(len(lines) - 1, -1, -1):
+ if lines[idx].strip() == "*** End Patch":
+ lines = lines[: idx + 1]
+ break
+ if not lines or lines[-1].strip() != "*** End Patch":
+ raise PatchApplyError("Patch must end with '*** End Patch'")
+
+ ops: List[Tuple[str, str, str]] = []
+ i = 1
+ while i < len(lines) - 1:
+ line = lines[i]
+ if line.startswith("*** Add File: "):
+ path = line[len("*** Add File: ") :].strip()
+ i += 1
+ buf: List[str] = []
+ while i < len(lines) - 1 and not lines[i].startswith("*** "):
+ # 兼容两种格式:
+ # 1) 规范形式:以 '+' 开头
+ # 2) 宽松形式:直接给出正文(模型有时会省略 '+')
+ if lines[i].startswith("+"):
+ buf.append(lines[i][1:] + "\n")
+ else:
+ buf.append(lines[i] + "\n")
+ i += 1
+ ops.append(("add", path, "".join(buf)))
+ continue
+ if line.startswith("*** Delete File: "):
+ path = line[len("*** Delete File: ") :].strip()
+ ops.append(("delete", path, ""))
+ i += 1
+ continue
+ if line.startswith("*** Update File: "):
+ path = line[len("*** Update File: ") :].strip()
+ i += 1
+ buf: List[str] = []
+ while i < len(lines) - 1 and not lines[i].startswith("*** "):
+ buf.append(lines[i])
+ i += 1
+ ops.append(("update", path, "\n".join(buf)))
+ continue
+ if line.strip() == "":
+ i += 1
+ continue
+ raise PatchApplyError(f"Unexpected patch line: {line}")
+
+ return ops
+
+ def _estimate_changed_lines(self, ops: List[Tuple[str, str, str]]) -> int:
+ """
+ 估算补丁操作的总变更行数。
+ 用于检查补丁大小是否超过限制。
+
+ 参数:
+ ops: 补丁操作列表
+
+ 返回:
+ int: 估算的总变更行数
+ """
+ changed = 0
+ for kind, _, payload in ops:
+ if kind == "add":
+ # 添加文件:按行数计算
+ changed += payload.count("\n")
+ elif kind == "delete":
+ # 删除文件:按1行计算
+ changed += 1
+ elif kind == "update":
+ # 更新文件:只计算+/-开头的变更行
+ for l in payload.splitlines():
+ if l.startswith("+") or l.startswith("-"):
+ changed += 1
+ return changed
+
+ def _apply_update_payload(self, original: List[str], payload: str, rel_path: str) -> List[str]:
+ """
+ 应用 Update File 的内容。
+ 将 payload 分割成多个 hunk (代码块),然后逐个应用。
+ """
+ # 兼容宽松格式:如果 payload 没有任何 + / - / 前导空格行,视为“整文件替换”
+ raw_lines = payload.splitlines(keepends=True)
+ if raw_lines and all(not l.startswith(("+", "-", " ")) for l in raw_lines):
+ return raw_lines
+
+ hunks = self._split_hunks(payload)
+ current = original
+ try:
+ for hunk in hunks:
+ current = self._apply_hunk(current, hunk, rel_path)
+ return current
+ except PatchApplyError as e:
+ # 宽松兜底:当上下文匹配失败时,尝试将 payload 视作“新的完整文件”生成 after 版本
+ if "context not found" not in str(e).lower():
+ raise
+ fallback = self._hunks_to_after(hunks)
+ if fallback:
+ return fallback
+ raise
+
+ def _split_hunks(self, payload: str) -> List[List[str]]:
+ """
+ 将 Update File 的 payload 分割成多个 hunk(代码块)。
+ Hunk 通常由 @@ ... @@ 分隔符分隔,或者由空行分隔。
+ 每个 hunk 代表文件的一个修改区域。
+
+ 参数:
+ payload: Update File 操作的内容
+
+ 返回:
+ List[List[str]]: hunk 列表,每个 hunk 是多行字符串的列表
+ """
+ lines = payload.splitlines()
+ hunks: List[List[str]] = []
+ buf: List[str] = []
+ for l in lines:
+ if l.startswith("@@"):
+ if buf:
+ hunks.append(buf)
+ buf = []
+ continue
+ if l.strip() == "" and buf:
+ hunks.append(buf)
+ buf = []
+ continue
+ buf.append(l)
+ if buf:
+ hunks.append(buf)
+ return [h for h in hunks if any(x.startswith((" ", "+", "-")) for x in h)]
+
+ def _apply_hunk(self, current: List[str], hunk_lines: List[str], rel_path: str) -> List[str]:
+ """
+ 应用单个 hunk(代码块)到当前文件内容。
+
+ 原理:
+ 1. 解析 hunk,分离出 'before' (上下文 + 删除行) 和 'after' (上下文 + 新增行)
+ 2. 在当前文件中查找 'before' 块的精确位置
+ 3. 如果找到匹配的上下文,用 'after' 块替换 'before' 块
+ 4. 如果找不到匹配的上下文,抛出异常
+
+ 参数:
+ current: 当前文件的内容行列表
+ hunk_lines: hunk 的内容行列表
+ rel_path: 文件的相对路径(用于错误提示)
+
+ 返回:
+ List[str]: 应用 hunk 后的文件内容行列表
+
+ 异常:
+ PatchApplyError: 当 hunk 格式错误或找不到匹配的上下文时抛出
+ """
+ before: List[str] = []
+ after: List[str] = []
+ for l in hunk_lines:
+ if not l:
+ continue
+ tag = l[0]
+ text = l[1:] + "\n"
+ if tag == " ":
+ before.append(text)
+ after.append(text)
+ elif tag == "-":
+ before.append(text)
+ elif tag == "+":
+ after.append(text)
+
+ if not before:
+ raise PatchApplyError("Update hunk has no context/removals; refusing to apply")
+
+ idx = self._find_subsequence(current, before)
+ if idx is None:
+ context_line = next((b.strip() for b in before if b.strip()), "")
+ hint = f"{rel_path}:search:'{context_line[:80]}'"
+ raise PatchApplyError("Patch hunk context not found; file changed?", recheck_targets=[hint])
+
+ return current[:idx] + after + current[idx + len(before) :]
+
+ def _find_subsequence(self, haystack: List[str], needle: List[str]) -> Optional[int]:
+ """
+ 在文件内容中查找代码块的起始位置。
+ 使用简单的 O(N*M) 字符串匹配算法,在 haystack 中查找 needle 的精确匹配。
+
+ 参数:
+ haystack: 文件内容行列表
+ needle: 要查找的代码块行列表
+
+ 返回:
+ Optional[int]: 匹配的起始行索引,如果未找到则返回 None
+ """
+ if len(needle) > len(haystack):
+ return None
+ for i in range(0, len(haystack) - len(needle) + 1):
+ if haystack[i : i + len(needle)] == needle:
+ return i
+ # 宽松匹配:忽略行尾空白再尝试一次,缓解缩进/换行轻微偏差
+ norm_hay = [h.rstrip() + "\n" for h in haystack]
+ norm_need = [n.rstrip() + "\n" for n in needle]
+ for i in range(0, len(norm_hay) - len(norm_need) + 1):
+ if norm_hay[i : i + len(norm_need)] == norm_need:
+ return i
+ return None
+
+ def _hunks_to_after(self, hunks: List[List[str]]) -> List[str]:
+ """
+ 将多个 hunk 的“after”部分合成为一份完整文件内容。
+ 用于上下文匹配失败时的宽松回退:保留 + 和空格行,忽略 - 行。
+ """
+ out: List[str] = []
+ for hunk in hunks:
+ for l in hunk:
+ if not l:
+ continue
+ tag = l[0]
+ text = l[1:] + "\n" if len(l) > 1 else "\n"
+ if tag == "-" or tag == "@":
+ continue
+ if tag in (" ", "+"):
+ out.append(text)
+ return out
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/hello_code_cli.py b/Co-creation-projects/aug618-Praxis/code_agent/hello_code_cli.py
new file mode 100644
index 00000000..ef2387fc
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/hello_code_cli.py
@@ -0,0 +1,376 @@
+from __future__ import annotations
+
+import argparse
+import os
+import logging
+import uuid
+from pathlib import Path
+
+try:
+ from dotenv import load_dotenv # type: ignore
+except Exception: # pragma: no cover
+ def load_dotenv(*args, **kwargs): # type: ignore
+ return False
+
+from core.llm import HelloAgentsLLM
+from core.exceptions import HelloAgentsException
+from core.config import Config, AVAILABLE_MODELS
+from code_agent.agentic import CodeAgent
+from code_agent.executors.apply_patch_executor import ApplyPatchExecutor, PatchApplyError
+from utils.cli_ui import c, hr, PRIMARY, ACCENT, INFO, WARN, ERROR
+from utils.env import env_str
+from utils.observability import log_event
+from utils.patch_utils import extract_patch, normalize_patch, patch_requires_confirmation
+from utils.session_utils import load_events, summarize_session, export_session
+
+
+
+
+def main(argv: list[str] | None = None) -> int:
+ """
+ CLI 入口点。
+ 初始化 LLM、CodebaseMaintainer 和 PatchExecutor,并进入交互式循环。
+ """
+ # 1. 解析命令行参数
+ parser = argparse.ArgumentParser(description=" Code Agent CLI (Codex/Claude-like)")
+ parser.add_argument("--repo", type=str, default=".", help="Repository root (workspace). Default: .")
+ parser.add_argument("--project", type=str, default=None, help="Project name (default: repo folder name)")
+ args = parser.parse_args(argv)
+
+ # 2. 初始化环境和 LLM
+ repo_root = Path(args.repo).resolve()
+ load_dotenv(dotenv_path=repo_root / ".env", override=False)
+
+ project = args.project or repo_root.name
+ config = Config.from_env()
+ llm = HelloAgentsLLM() # auto-detect provider from env
+ # reduce noisy HTTP client logs in the CLI
+ logging.getLogger("httpx").setLevel(logging.WARNING)
+ logging.getLogger("openai").setLevel(logging.WARNING)
+ logging.getLogger("openai._base_client").setLevel(logging.WARNING)
+ logging.getLogger("memory").setLevel(logging.WARNING)
+
+ session_id = uuid.uuid4().hex
+ os.environ["CODE_AGENT_SESSION_ID"] = session_id
+ turns = 0
+
+ def _end_session(reason: str, exit_code: int, error: str | None = None):
+ payload = {
+ "reason": reason,
+ "exit_code": exit_code,
+ "turns": turns,
+ "project": project,
+ "workspace": str(repo_root),
+ "model": llm.model,
+ "provider": llm.provider,
+ }
+ if error:
+ payload["error"] = error
+ log_event("session_end", payload)
+
+ log_event(
+ "session_start",
+ {
+ "project": project,
+ "workspace": str(repo_root),
+ "model": llm.model,
+ "provider": llm.provider,
+ },
+ )
+
+ print(c(hr("=", 80), INFO))
+ print(c("神秘奇奶龙-你的code管家", PRIMARY))
+ print()
+ print(c(f"workspace: {repo_root}", INFO))
+ print()
+ model_type = "多模态" if llm.is_multimodal else "文本"
+ print(c(f"当前模型选择: {llm.model} ({model_type})", INFO))
+ print()
+ print(c(f"保存状态目录: {Path(config.helloagents_dir).as_posix()}", INFO))
+ print(c(hr("=", 80), INFO))
+
+ # Optional preflight to surface auth issues early.
+ try:
+ _ = llm.invoke([{"role": "user", "content": "ping"}], max_tokens=1)
+ except HelloAgentsException as e:
+ print(c("LLM 预检失败(通常是 API key/base_url/model 配置问题)。", ERROR))
+ print(c(f"error: {e}", ERROR))
+ print(c("请检查 .env 中的 DEEPSEEK_API_KEY / LLM_* 配置是否正确。", WARN))
+ _end_session("preflight_failed", 2, error=str(e))
+ return 2
+
+ # 3. 初始化核心组件(ReAct + tools)
+ agent = CodeAgent(repo_root=repo_root, llm=llm, config=config)
+ patch_executor = ApplyPatchExecutor(repo_root=repo_root)
+
+ # 4. 进入交互循环
+ print(c("输入自然语言需求开始,以下是命令:", INFO))
+ print(c(" /quit", ACCENT) + c(" 退出", INFO))
+ print(c(" /plan <目标> [--save]", ACCENT) + c(" 强制生成计划(可保存)", INFO))
+ print(c(" /model", ACCENT) + c(" 查看/切换模型(多模态模型直接识图,文本模型走 OCR)", INFO))
+ print(c(" /stats [current|last|]", ACCENT) + c(" 查看会话统计", INFO))
+ print(c(" /export [current|last|]", ACCENT) + c(" 导出会话信息", INFO))
+ print()
+ print(c("@ 引用语法(多个用逗号/顿号分隔):", INFO))
+ print(c(" @file(a.py, b.png)", ACCENT) + c(" 引用文件(支持图片、代码等)", INFO))
+ print(c(" @dir(src/, lib/)", ACCENT) + c(" 引用目录(列出结构+关键文件)", INFO))
+ print(c(" 示例: @file(main.py, image.png) @dir(src/) 请分析这些代码", INFO))
+ try:
+ while True:
+ try:
+ user_in = input(c(" 😅(你想干嘛?): ", PRIMARY))
+ except (EOFError, KeyboardInterrupt):
+ print("\n" + c("电脑没油了,下次再见", INFO))
+ _end_session("user_exit", 0)
+ return 0
+
+ if user_in is None:
+ continue
+ user_in = user_in.strip()
+ if not user_in:
+ print(c("请提供具体指令或问题。", WARN))
+ continue
+ turns += 1
+ if user_in in {"/q", "/quit", "quit", "exit"}:
+ print()
+ print(c("没钱充token了,下次再见", INFO))
+ _end_session("user_exit", 0)
+ return 0
+ if user_in.startswith("/stats"):
+ arg = user_in[len("/stats"):].strip()
+ log_dir = env_str("CODE_AGENT_LOG_DIR") or str(Path(".helloagents") / "logs")
+ log_path = Path(log_dir) / "events.jsonl"
+ events = load_events(log_path)
+ if not events:
+ print(c("暂无日志数据。", WARN))
+ continue
+
+ current_id = env_str("CODE_AGENT_SESSION_ID")
+ target_id = None
+ if arg == "current" or not arg:
+ target_id = current_id
+ elif arg == "last":
+ # 取最后一个 session_end 的 session_id
+ for e in reversed(events):
+ if e.get("type") == "session_end":
+ target_id = e.get("session_id")
+ break
+ else:
+ target_id = arg
+
+ if not target_id:
+ print(c("未找到目标会话。", WARN))
+ continue
+
+ session_events = [e for e in events if e.get("session_id") == target_id]
+ if not session_events:
+ print(c(f"未找到会话: {target_id}", WARN))
+ continue
+
+ stats = summarize_session(session_events)
+ print(c("📊 会话统计", PRIMARY))
+ print(c(f"session_id: {target_id}", INFO))
+ if stats["start_ts"]:
+ print(c(f"start: {stats['start_ts']}", INFO))
+ if stats["end_ts"]:
+ print(c(f"end: {stats['end_ts']}", INFO))
+ if stats["duration_ms"] is not None:
+ print(c(f"duration: {stats['duration_ms']} ms", INFO))
+ print(c(f"turns: {stats['turns']}", INFO))
+ print(c(f"tool_calls: {stats['tool_calls']} (errors: {stats['tool_errors']})", INFO))
+ print(c(f"llm_calls: {stats['llm_calls']} (errors: {stats['llm_errors']})", INFO))
+ if stats["prompt_tokens"] or stats["completion_tokens"]:
+ print(c(f"tokens: prompt={stats['prompt_tokens']} completion={stats['completion_tokens']}", INFO))
+ print(c(f"tokens_est: prompt≈{stats['prompt_tokens_est']} completion≈{stats['completion_tokens_est']}", INFO))
+ continue
+ if user_in.startswith("/export"):
+ arg = user_in[len("/export"):].strip()
+ log_dir = env_str("CODE_AGENT_LOG_DIR") or str(Path(".helloagents") / "logs")
+ log_path = Path(log_dir) / "events.jsonl"
+ events = load_events(log_path)
+ if not events:
+ print(c("暂无日志数据。", WARN))
+ continue
+
+ current_id = env_str("CODE_AGENT_SESSION_ID")
+ target_id = None
+ if arg == "current" or not arg:
+ target_id = current_id
+ elif arg == "last":
+ for e in reversed(events):
+ if e.get("type") == "session_end":
+ target_id = e.get("session_id")
+ break
+ else:
+ target_id = arg
+
+ if not target_id:
+ print(c("未找到目标会话。", WARN))
+ continue
+
+ session_events = [e for e in events if e.get("session_id") == target_id]
+ if not session_events:
+ print(c(f"未找到会话: {target_id}", WARN))
+ continue
+
+ export_dir = Path(log_dir).parent / "exports"
+ export_path = export_session(target_id, session_events, export_dir)
+ print(c("✅ 已导出会话信息", PRIMARY))
+ print(c(f"path: {export_path}", INFO))
+ continue
+ if user_in.startswith("/plan"):
+ raw = user_in[len("/plan") :].strip()
+ save_plan = False
+ if "--save" in raw:
+ save_plan = True
+ raw = raw.replace("--save", "").strip()
+ goal = raw or "请为当前任务生成一个可执行计划"
+ response = agent.registry.execute_tool("plan", goal)
+ print("\n" + c("🤖 plan", PRIMARY))
+ print(response + "\n")
+ if save_plan:
+ agent.note_tool.run({
+ "action": "create",
+ "title": "Plan",
+ "content": f"Goal:\n{goal}\n\nPlan:\n\n{response}",
+ "note_type": "plan",
+ "tags": [project, "plan"],
+ })
+ print(c("✅ 已保存到 notes", INFO))
+ continue
+
+ # 模型切换命令
+ if user_in == "/model":
+ model_list = list(AVAILABLE_MODELS.items())
+
+ # 显示当前模型和可用模型列表
+ model_type = "多模态 📷" if llm.is_multimodal else "文本 📝"
+ print(f"\n当前模型: {c(llm.model, PRIMARY)} ({model_type})")
+ print(f"\n可用模型:")
+ for i, (name, info) in enumerate(model_list, 1):
+ marker = "→ " if name == llm.model else " "
+ mtype = "多模态" if info["multimodal"] else "文本"
+ print(f" {marker}[{i}] {c(name, ACCENT)} [{mtype}]")
+
+ # 交互式选择
+ try:
+ choice = input(c("\n输入数字或模型名切换(回车取消): ", INFO)).strip()
+ except (EOFError, KeyboardInterrupt):
+ print()
+ continue
+
+ if not choice:
+ continue
+
+ # 解析选择
+ target_model = None
+ if choice.isdigit():
+ idx = int(choice) - 1
+ if 0 <= idx < len(model_list):
+ target_model = model_list[idx][0]
+ else:
+ print(c(f"无效序号(范围 1-{len(model_list)})", ERROR))
+ continue
+ elif choice in AVAILABLE_MODELS:
+ target_model = choice
+ else:
+ print(c(f"未知模型: {choice}", ERROR))
+ continue
+
+ if target_model:
+ old_key = llm.api_key
+ llm.switch_model(target_model)
+ model_type = "多模态 📷" if llm.is_multimodal else "文本 📝"
+ print(c(f"✓ 已切换到: {target_model} ({model_type})", PRIMARY))
+
+ # 提示 API key 状态
+ if llm.api_key and llm.api_key != old_key:
+ print(c(f" API Key: 已自动切换 ({llm.api_key[:8]}...)", INFO))
+ elif llm.api_key:
+ print(c(f" API Key: 使用当前配置 ({llm.api_key[:8]}...)", INFO))
+ else:
+ info = AVAILABLE_MODELS.get(target_model, {})
+ env_names = info.get("api_key_env", [])
+ if env_names:
+ print(c(f" ⚠️ 未找到 API Key,请配置: {', '.join(env_names)}", WARN))
+
+ if llm.is_multimodal:
+ print(c(" 图片将直接发送给 LLM 进行理解", INFO))
+ else:
+ print(c(" 图片将通过 OCR 提取文字后处理", INFO))
+ continue
+
+ # 5. 运行一轮对话(ReAct 可能按需调用终端/笔记/记忆)
+ # @file/@dir 引用会在 CodeAgent.run_turn 内部解析
+ try:
+ response = agent.run_turn(user_in)
+ except FileNotFoundError as e:
+ print(c(f"文件不存在:{e}", ERROR))
+ print(c("提示:使用 @file(路径) 引用文件,例如 @file(main.py, image.png) 请分析", WARN))
+ continue
+ except HelloAgentsException as e:
+ print(c(f"LLM 调用失败: {e}", ERROR))
+ continue
+
+ # 对于 direct reply(未经过 ReAct 的控制台打印),在 CLI 里补打一份输出
+ if getattr(agent, "last_direct_reply", False):
+ print(c("🤖 assistant", PRIMARY))
+ print(response)
+
+ # 7. 提取并应用补丁
+ patch_text = extract_patch(response)
+ if not patch_text:
+ continue
+ patch_text = normalize_patch(patch_text)
+ # Ignore empty patch blocks
+ if patch_text.strip() == "*** Begin Patch\n*** End Patch":
+ continue
+
+ needs_confirm = patch_requires_confirmation(patch_text)
+ if needs_confirm:
+ # If user just answered y/n as the *current* input, treat it as confirmation for this patch.
+ if user_in.strip().lower() in {"n", "no"}:
+ print("已取消补丁应用。")
+ continue
+ if user_in.strip().lower() not in {"y", "yes"}:
+ print("\n⚠️ 检测到高风险补丁(删除/大规模变更)。是否应用?(y/n)")
+ ans = input("confirm> ").strip().lower()
+ if ans not in {"y", "yes"}:
+ print("已取消补丁应用。")
+ continue
+
+ try:
+ res = patch_executor.apply(patch_text)
+ print("\n" + c("✅ Patch applied", PRIMARY))
+ print(c(f"files: {', '.join(res.files_changed) if res.files_changed else '(none)'}", INFO))
+ if res.backups:
+ print(c(f"backups: {len(res.backups)} (in .helloagents/backups/...)", INFO))
+
+ # 记录到 NoteTool(action)
+ agent.note_tool.run({
+ "action": "create",
+ "title": "Patch applied",
+ "content": f"User input:\n{user_in}\n\nPatch:\n\n```text\n{patch_text}\n```\n\nFiles:\n"
+ + "\n".join([f"- {p}" for p in res.files_changed]),
+ "note_type": "action",
+ "tags": [project, "patch_applied"],
+ })
+ except PatchApplyError as e:
+ print("\n" + c(f"❌ Patch failed: {e}", ERROR))
+ agent.note_tool.run({
+ "action": "create",
+ "title": "Patch failed",
+ "content": f"Error: {e}\n\nUser input:\n{user_in}\n\nPatch:\n\n```text\n{patch_text}\n```\n",
+ "note_type": "blocker",
+ "tags": [project, "patch_failed"],
+ })
+ continue
+
+ except Exception as e:
+ print(c(f"❌ CLI 异常退出: {e}", ERROR))
+ _end_session("crash", 1, error=str(e))
+ return 1
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/hello_code_tui.py b/Co-creation-projects/aug618-Praxis/code_agent/hello_code_tui.py
new file mode 100644
index 00000000..3cbb39d6
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/hello_code_tui.py
@@ -0,0 +1,1589 @@
+"""HelloAgents Code Agent TUI - Main Logic.
+
+TUI entry point and main application class.
+Styles and utilities are in utils/tui_ui.py.
+"""
+
+from __future__ import annotations
+
+import argparse
+import asyncio
+import contextlib
+import io
+import json
+import os
+import re
+import time
+import uuid
+from pathlib import Path
+from typing import Iterable, Optional
+
+try:
+ from dotenv import load_dotenv # type: ignore
+except Exception: # pragma: no cover
+ def load_dotenv(*args, **kwargs): # type: ignore
+ return False
+
+from rich import box
+from rich.align import Align
+from rich.panel import Panel
+from rich.text import Text
+from textual.app import App, ComposeResult
+from textual.binding import Binding
+from textual.containers import Horizontal, Vertical, VerticalScroll
+from textual.widget import Widget
+from textual.widgets import Header, Input, RichLog, ListView, ListItem, Static, Collapsible
+
+from core.config import Config, AVAILABLE_MODELS
+from core.exceptions import HelloAgentsException
+from core.llm import HelloAgentsLLM
+from code_agent.agentic import CodeAgent
+from code_agent.executors.apply_patch_executor import ApplyPatchExecutor, PatchApplyError
+from utils.env import env_flag, env_flag_true, env_lower, env_str, env_stripped
+from utils.observability import log_event
+from utils.tui_ui import (
+ TUI_CSS,
+ extract_patch,
+ normalize_patch,
+ load_events,
+ summarize_session,
+ export_session,
+)
+
+
+ANSI_RE = re.compile(r"\x1b\[[0-9;]*m")
+CONFIRM_TOOL_RE = re.compile(r"\[\[CONFIRM_TOOL\]\]([\s\S]*?)\[\[/CONFIRM_TOOL\]\]")
+
+
+def _strip_ansi(text: str) -> str:
+ return ANSI_RE.sub("", text or "")
+class _StreamingTUIWriter(io.TextIOBase):
+ """A stdout/stderr-like stream that forwards output to the TUI in near-real-time.
+
+ Why: `RichLog` can only append new lines, so we buffer partial writes and flush
+ on newlines (or when the buffer grows / stalls), to avoid UI freezing and avoid
+ dumping everything at the end.
+ """
+
+ def __init__(self, app: "CodeAgentTUI", kind: str) -> None:
+ super().__init__()
+ self._app = app
+ self._kind = kind # "stdout" | "stderr"
+ self._buf: str = ""
+ self._last_emit_ts = 0.0
+
+ def writable(self) -> bool: # pragma: no cover
+ return True
+
+ def isatty(self) -> bool: # pragma: no cover
+ return False
+
+ def write(self, s: str) -> int: # type: ignore[override]
+ if not s:
+ return 0
+ self._buf += s
+ self._drain_lines()
+ self._maybe_emit_partial()
+ return len(s)
+
+ def flush(self) -> None: # type: ignore[override]
+ # Many libraries call flush very frequently (e.g. token streaming).
+ # We treat flush as a "maybe" signal to update, not a hard boundary.
+ self._drain_lines()
+ self._maybe_emit_partial()
+
+ def finish(self) -> None:
+ """Force emit all remaining buffered content (call at turn end)."""
+ self._drain_lines()
+ self._emit(self._buf)
+ self._buf = ""
+
+ def _drain_lines(self) -> None:
+ while "\n" in self._buf:
+ line, rest = self._buf.split("\n", 1)
+ self._buf = rest
+ self._emit(line)
+
+ def _maybe_emit_partial(self) -> None:
+ if not self._buf:
+ return
+ now = time.time()
+ # Emit partial buffer only if it is large or has been waiting for a while.
+ if len(self._buf) >= 800 or (now - self._last_emit_ts) >= 1.0:
+ self._emit(self._buf)
+ self._buf = ""
+
+ def _emit(self, chunk: str) -> None:
+ if chunk is None:
+ return
+ chunk = _strip_ansi(chunk)
+ if chunk == "":
+ # Keep blank lines (progress separators).
+ payload = ""
+ else:
+ payload = chunk.rstrip("\r")
+ self._last_emit_ts = time.time()
+
+ def _do() -> None:
+ self._app._write_stream(payload, kind=self._kind)
+
+ # If we're inside a background thread, schedule to UI thread.
+ try:
+ self._app.call_from_thread(_do) # type: ignore[attr-defined]
+ except Exception:
+ _do()
+
+
+class SuggestionItem(ListItem):
+ """Custom list item that stores the suggestion value and optional description."""
+
+ def __init__(self, value: str, description: str = "") -> None:
+ super().__init__()
+ self.value = value
+ self.description = description
+
+ def compose(self) -> ComposeResult:
+ if self.description:
+ # Two-column layout: command + description
+ yield Static(f"[cyan]{self.value:<20}[/cyan] [dim]{self.description}[/dim]", markup=True)
+ else:
+ yield Static(self.value)
+
+
+# Available commands with descriptions
+COMMANDS = [
+ ("/model", "查看或切换模型"),
+ ("/plan", "生成执行计划 (--save 保存)"),
+ ("/stats", "查看会话统计"),
+ ("/export", "导出会话数据"),
+ ("/clear", "清空输出"),
+ ("/quit", "退出"),
+]
+
+
+class CodeAgentTUI(App):
+ """Main TUI application for HelloAgents Code Agent."""
+
+ CSS = TUI_CSS
+ TITLE = "神秘奇奶龙--你的code管家"
+ ENABLE_COMMAND_PALETTE = False # 移除右下角 palette 提示
+
+ BINDINGS = [
+ Binding("ctrl+c", "quit", "Quit", show=True),
+ Binding("ctrl+q", "quit", "Quit", show=False),
+ Binding("ctrl+l", "toggle_logo", "Logo", show=False),
+ Binding("ctrl+t", "toggle_trace", "Trace", show=False),
+ Binding("tab", "complete", "Complete", show=False),
+ Binding("up", "suggestion_up", "Up", show=False),
+ Binding("down", "suggestion_down", "Down", show=False),
+ Binding("escape", "hide_suggestions", "Hide", show=False),
+ ]
+
+ def __init__(self, repo_root: Path, project: str | None = None):
+ super().__init__()
+ # TUI 默认静默:避免启动时刷屏(需要时可在环境变量里显式关闭)
+ os.environ.setdefault("CODE_AGENT_QUIET", "1")
+ self.repo_root = repo_root.resolve()
+ self.project = project or self.repo_root.name
+ self.config = Config.from_env()
+ self.llm = HelloAgentsLLM()
+ self.agent = CodeAgent(repo_root=self.repo_root, llm=self.llm, config=self.config)
+ self.patch_executor = ApplyPatchExecutor(repo_root=self.repo_root)
+ self.session_id = uuid.uuid4().hex
+ os.environ["CODE_AGENT_SESSION_ID"] = self.session_id
+ self.turns = 0
+
+ self.pending_patch_text: str | None = None
+ self.pending_user_input: str | None = None
+ self.pending_tool_name: str | None = None
+ self.pending_tool_input: str | None = None
+ self.pending_tool_user_input: str | None = None
+ self.pending_bang_command: str | None = None
+
+ self._completion_start: Optional[int] = None
+ self._completion_tag: Optional[str] = None
+ self._suggestions: list[str] = []
+ self._busy: bool = False
+ self._logo_frames: list[Text] = []
+ self._logo_frame_idx: int = 0
+ self._logo_timer = None
+ self._logo_visibility: str = env_lower("CODE_AGENT_LOGO_VISIBILITY", "once") or "once"
+ if self._logo_visibility not in {"always", "once", "never"}:
+ self._logo_visibility = "once"
+ self._logo_splash_timer = None
+ self._trace_offset: int = 0
+ self._trace_path: Path | None = None
+ self._trace_enabled: bool = env_flag_true("CODE_AGENT_TRACE_ENABLED", default=True)
+ self._thought_log: RichLog | None = None
+
+ def compose(self) -> ComposeResult:
+ yield Header()
+ with Vertical():
+ yield Static("", id="logo")
+ yield RichLog(id="trace", wrap=True, markup=True)
+ yield VerticalScroll(id="output")
+ yield ListView(id="suggestions")
+ with Vertical(id="input_area"):
+ yield Static("", id="input_line_top")
+ with Horizontal(id="input_row"):
+ yield Static(">", id="input_prompt")
+ # placeholder 过长 + focus 样式在部分终端会呈现“色块/乱码”,这里缩短文案降低渲染风险
+ yield Input(placeholder="输入消息(/命令,@引用)", id="input_bar")
+ yield Static("", id="input_line_bottom")
+ # 不显示底部快捷键栏(用户不需要 q quit)
+
+ def on_mount(self) -> None:
+ log_event(
+ "session_start",
+ {
+ "project": self.project,
+ "workspace": str(self.repo_root),
+ "model": self.llm.model,
+ "provider": self.llm.provider,
+ },
+ )
+ # 首次 layout 完成后再渲染横线,保证长度等于终端宽度
+ try:
+ self.call_after_refresh(self._update_input_lines) # type: ignore[attr-defined]
+ except Exception:
+ try:
+ self.set_timer(0, self._update_input_lines) # type: ignore[attr-defined]
+ except Exception:
+ self._update_input_lines()
+
+ # 启动 Logo(像项目 banner 一样,每次启动最开始显示)
+ if self._logo_visibility != "never":
+ self._write_logo()
+ self._maybe_auto_hide_logo()
+ else:
+ self._set_logo_visible(False)
+
+ # Trace timeline:从 events.jsonl 增量读取当前会话事件
+ if self._trace_enabled:
+ self._init_trace_timeline()
+
+ # 输出文案:TUI 更强调可读性(用户能快速定位 user/assistant/过程日志)
+ self._write_rule(
+ "欢迎使用:神秘奇奶龙--你的 code 管家",
+ border_style="#7aa2f7",
+ title_style="bold #7aa2f7",
+ )
+ self._write("")
+ self._write_kv(" 工作根目录", str(self.repo_root))
+ self._write("")
+ model_type = "多模态" if self.llm.is_multimodal else "文本"
+ self._write_kv(" 当前模型", f"{self.llm.model} ({model_type})")
+ self._write("")
+ self._write_kv(" 状态保存目录", Path(self.config.helloagents_dir).as_posix())
+ self._write("")
+
+ self._write_rule("提示:命令以 / 开头;引用文件、目录用 @(空格分隔)", border_style="#e0af68", title_style="bold #e0af68")
+
+ self._write("")
+
+ # Focus input
+ self.query_one("#input_bar", Input).focus()
+
+ def action_toggle_logo(self) -> None:
+ """Toggle logo visibility (Ctrl+L)."""
+ logo = self.query_one("#logo", Static)
+ self._set_logo_visible(not bool(getattr(logo, "display", True)))
+
+ def action_toggle_trace(self) -> None:
+ """Toggle trace panel visibility (Ctrl+T)."""
+ trace = self.query_one("#trace", RichLog)
+ trace.display = not bool(getattr(trace, "display", True))
+
+ def _init_trace_timeline(self) -> None:
+ log_dir = env_str("CODE_AGENT_LOG_DIR") or str(self.repo_root / ".helloagents" / "logs")
+ self._trace_path = (Path(log_dir).expanduser().resolve() / "events.jsonl")
+ self._trace_offset = 0
+ trace = self.query_one("#trace", RichLog)
+ trace.clear()
+ trace.write(Text("Trace Timeline(Ctrl+T 展开/折叠)", style="dim"))
+
+ def _poll() -> None:
+ self._poll_trace_events()
+
+ try:
+ self.set_interval(0.5, _poll) # type: ignore[attr-defined]
+ except Exception:
+ pass
+
+ def _poll_trace_events(self) -> None:
+ if not self._trace_path:
+ return
+ try:
+ if not self._trace_path.exists():
+ return
+ with self._trace_path.open("r", encoding="utf-8", errors="ignore") as f:
+ f.seek(self._trace_offset)
+ chunk = f.read()
+ self._trace_offset = f.tell()
+ except Exception:
+ return
+
+ if not chunk:
+ return
+
+ trace = self.query_one("#trace", RichLog)
+ for line in chunk.splitlines():
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ import json as _json
+
+ e = _json.loads(line)
+ except Exception:
+ continue
+ if e.get("session_id") != self.session_id:
+ continue
+
+ et = e.get("type")
+ ts = (e.get("ts") or "")[-12:-1] # HH:MM:SS.mmm approx
+ if et == "tool":
+ ok = "✓" if e.get("ok", True) else "✗"
+ tool = e.get("tool")
+ ms = e.get("ms")
+ call_id = e.get("tool_call_id") or ""
+ inp = e.get("input_preview") or e.get("input")
+ outp = e.get("output_preview")
+ header = Text(
+ f"{ts} {ok} tool {tool} ({ms} ms) #{call_id}",
+ style="#7aa2f7" if ok == "✓" else "red",
+ )
+ trace.write(header)
+ if inp:
+ trace.write(Text(f" in: {str(inp)[:240]}", style="dim"))
+ if outp:
+ trace.write(Text(f" out: {str(outp)[:240]}", style="dim"))
+ elif et == "llm":
+ ok = "✓" if e.get("ok", True) else "✗"
+ ms = e.get("ms")
+ model = e.get("model")
+ pt = e.get("prompt_tokens") or e.get("prompt_tokens_est")
+ ct = e.get("completion_tokens") or e.get("completion_tokens_est")
+ trace.write(
+ Text(
+ f"{ts} {ok} llm {model} ({ms} ms) tokens≈{pt}/{ct}",
+ style="#bb9af7" if ok == "✓" else "red",
+ )
+ )
+ elif et in {"patch_apply", "verify"}:
+ ok = "✓" if e.get("ok", True) else "✗"
+ ms = e.get("ms")
+ trace.write(
+ Text(
+ f"{ts} {ok} {et} ({ms} ms) {e.get('summary','')}",
+ style="green" if ok == "✓" else "red",
+ )
+ )
+ else:
+ if et in {"context_base", "session_start", "session_end"}:
+ trace.write(Text(f"{ts} • {et}", style="dim"))
+
+ def _set_logo_visible(self, visible: bool) -> None:
+ logo = self.query_one("#logo", Static)
+ logo.display = visible
+ # If hidden, stop animation timer to avoid wasting CPU.
+ if not visible:
+ try:
+ if self._logo_timer is not None:
+ self._logo_timer.stop() # type: ignore[attr-defined]
+ except Exception:
+ pass
+ self._logo_timer = None
+
+ def _maybe_auto_hide_logo(self) -> None:
+ """If visibility=once, auto-hide logo after a short splash."""
+ if self._logo_visibility != "once":
+ return
+ sec_s = env_stripped("CODE_AGENT_LOGO_SPLASH_SECONDS", "")
+ try:
+ sec = float(sec_s) if sec_s else 2.0
+ except Exception:
+ sec = 2.0
+ sec = max(0.2, min(10.0, sec))
+
+ def _hide() -> None:
+ self._set_logo_visible(False)
+
+ try:
+ self._logo_splash_timer = self.set_timer(sec, _hide) # type: ignore[attr-defined]
+ except Exception:
+ # If timers are unavailable, just leave it visible.
+ self._logo_splash_timer = None
+
+ def on_resize(self) -> None:
+ # Keep gradient lines aligned with terminal width
+ self._update_input_lines()
+
+ def on_unmount(self) -> None:
+ log_event(
+ "session_end",
+ {
+ "reason": "user_exit",
+ "exit_code": 0,
+ "turns": self.turns,
+ "project": self.project,
+ "workspace": str(self.repo_root),
+ "model": self.llm.model,
+ "provider": self.llm.provider,
+ },
+ )
+
+ def _update_input_lines(self) -> None:
+ """Render cyan 'gradient' lines like iflow (via Rich Text segments)."""
+ # Always match terminal width; keep 1 char margin to avoid wrapping.
+ width = max(10, (self.size.width or 0) - 1)
+ chars = "─" * width
+ # Make lines brighter/whiter (user wants white lines)
+ colors = ["#ffffff", "#e6f7ff", "#b3f5ff", "#e6f7ff", "#ffffff"]
+ seg = max(1, width // len(colors))
+
+ line = Text()
+ i = 0
+ for idx, c in enumerate(colors):
+ # last segment takes remainder
+ if idx == len(colors) - 1:
+ chunk = chars[i:]
+ else:
+ chunk = chars[i : i + seg]
+ i += len(chunk)
+ if chunk:
+ line.append(chunk, style=c)
+
+ self.query_one("#input_line_top", Static).update(line)
+ self.query_one("#input_line_bottom", Static).update(line)
+
+ # ============================================================
+ # Output helpers
+ # ============================================================
+
+ def _write(self, text: str, style: str | None = None, *, markup: bool = False) -> None:
+ """写入输出区(可交互:支持折叠、点击等)。"""
+ renderable = Text.from_markup(text if text is not None else "", style=style) if markup else Text(text if text is not None else "", style=style)
+ self._mount_output(Static(renderable))
+
+ def _write_rule(
+ self,
+ title: str,
+ *,
+ border_style: str = "#202637",
+ title_style: str = "bold",
+ ) -> None:
+ self._mount_output(
+ Static(
+ Panel(
+ Text(title, style=title_style),
+ box=box.ROUNDED,
+ border_style=border_style,
+ padding=(0, 1),
+ )
+ )
+ )
+
+ def _write_logo(self) -> None:
+ """在启动时输出一个“项目 Logo”。
+
+ 优先级:
+ 1) 环境变量 `CODE_AGENT_LOGO` 指定的图片路径
+ 2) repo 内可提交的常见路径(如 images/ 或 assets/)
+ 3) 兜底:内置 ASCII art
+ """
+
+ def _candidate_paths() -> list[Path]:
+ candidates: list[Path] = []
+ env_logo = env_stripped("CODE_AGENT_LOGO", "")
+ if env_logo:
+ candidates.append(Path(env_logo).expanduser())
+ # common defaults
+ candidates.extend(
+ [
+ self.repo_root / "assets" / "logo.png",
+ self.repo_root / "assets" / "logo.jpg",
+ self.repo_root / "assets" / "logo.gif",
+ self.repo_root / "images" / "logo.png",
+ self.repo_root / "images" / "logo.jpg",
+ self.repo_root / "images" / "logo.gif",
+ self.repo_root / "images" / "nailong.png",
+ self.repo_root / "images" / "nailong.jpg",
+ self.repo_root / "images" / "nailong.gif",
+ ]
+ )
+ return candidates
+
+ def _render_image_to_text(img_rgb, width: int) -> Text | None:
+ try:
+ from PIL import Image # type: ignore
+ except Exception:
+ return None
+ img = img_rgb
+ if not isinstance(img, Image.Image):
+ return None
+
+ w0, h0 = img.size
+ if w0 <= 0 or h0 <= 0:
+ return None
+
+ # Render with upper-half blocks: each char represents 2 vertical pixels.
+ # We scale height in pixels by ~2 so the final character aspect looks OK.
+ height_px = max(2, int(h0 / w0 * width * 2))
+ if height_px % 2 == 1:
+ height_px += 1
+ img = img.resize((width, height_px))
+ px = img.load()
+ if px is None:
+ return None
+
+ out = Text()
+ for y in range(0, height_px, 2):
+ for x in range(width):
+ r1, g1, b1 = px[x, y]
+ r2, g2, b2 = px[x, y + 1]
+ style = f"#{r1:02x}{g1:02x}{b1:02x} on #{r2:02x}{g2:02x}{b2:02x}"
+ out.append("▀", style=style)
+ out.append("\n")
+ return out
+
+ def _render_image_to_dots(img_rgb, width_chars: int) -> Text | None:
+ """彩色点阵渲染:把图片压到字符网格,用彩色 '•' 表达像素点。"""
+ try:
+ from PIL import Image # type: ignore
+ except Exception:
+ return None
+ img = img_rgb
+ if not isinstance(img, Image.Image):
+ return None
+
+ w0, h0 = img.size
+ if w0 <= 0 or h0 <= 0:
+ return None
+
+ width_chars = max(2, int(width_chars))
+ # 字符格通常“高于宽”,用一个经验系数避免看起来被拉长
+ aspect = float(env_str("CODE_AGENT_LOGO_DOT_ASPECT", "0.55"))
+ height_chars = max(1, int(h0 / w0 * width_chars * aspect))
+
+ img = img.resize((width_chars, height_chars)).convert("RGB")
+ px = img.load()
+ if px is None:
+ return None
+
+ dot_char = env_str("CODE_AGENT_LOGO_DOT_CHAR", "•")
+ out = Text()
+ for y in range(height_chars):
+ for x in range(width_chars):
+ r, g, b = px[x, y]
+ out.append(dot_char, style=f"#{r:02x}{g:02x}{b:02x}")
+ out.append("\n")
+ return out
+
+ def _fit_width_no_upscale(w0: int, h0: int, *, mode: str) -> int:
+ """尽量保持原图尺寸:不放大,仅在超出可用区域时等比缩小。
+
+ 说明:终端显示是“字符格”,无法真正按原始像素大小展示;这里的“原图尺寸”
+ 指尽量少做缩小,只要终端放得下就不缩。
+ """
+ # 可用宽度(字符格)
+ max_w = max(10, (self.size.width or 80) - 8)
+
+ # 默认:尽量给 logo 更多空间(只要终端高度允许)
+ # 也可用环境变量覆盖
+ env_max_h = env_stripped("CODE_AGENT_LOGO_MAX_HEIGHT", "")
+ if env_max_h.isdigit():
+ max_h_lines = max(6, int(env_max_h))
+ else:
+ # 预留若干行给 output/input;剩余尽量给 logo
+ reserved = 14
+ max_h_lines = max(8, min(40, max(8, (self.size.height or 40) - reserved)))
+
+ target_w = min(w0, max_w) # 不放大
+ target_w = max(2, target_w)
+
+ # 高度约束(可关闭):不同模式的“每列宽度对应的行数”不同
+ # halfblock: height_lines ≈ h0/w0 * target_w
+ # dot: height_lines ≈ h0/w0 * target_w * aspect(默认 0.55)
+ disable_h_limit = env_flag("CODE_AGENT_LOGO_DISABLE_HEIGHT_LIMIT", default=False)
+ if not disable_h_limit and h0 > 0:
+ if mode == "dot":
+ try:
+ aspect = float(env_str("CODE_AGENT_LOGO_DOT_ASPECT", "0.55"))
+ except Exception:
+ aspect = 0.55
+ aspect = max(0.2, min(2.0, aspect))
+ height_lines_per_w = (h0 / w0) * aspect
+ else:
+ height_lines_per_w = (h0 / w0)
+ if height_lines_per_w > 0:
+ max_w_by_h = int(max_h_lines / height_lines_per_w)
+ if max_w_by_h > 0:
+ target_w = min(target_w, max_w_by_h)
+
+ # 允许强制指定宽度(不推荐过大,可能溢出)
+ env_w = env_stripped("CODE_AGENT_LOGO_WIDTH", "")
+ if env_w.isdigit():
+ forced = int(env_w)
+ if forced > 0:
+ target_w = min(max_w, forced)
+
+ return max(2, target_w)
+
+ # 如果之前有动画 timer,先停掉
+ try:
+ if self._logo_timer is not None:
+ self._logo_timer.stop() # type: ignore[attr-defined]
+ except Exception:
+ pass
+ self._logo_timer = None
+ self._logo_frames = []
+ self._logo_frame_idx = 0
+
+ # 三种模式(由环境变量控制):
+ # - image: 渲染静态图片(即使输入是 gif,也只取首帧)
+ # - gif: 渲染 gif 动图(若输入非动图则退化为 image)
+ # - dot: 彩色点阵(即使输入是 gif,也只取首帧)
+ logo_mode = env_lower("CODE_AGENT_LOGO_MODE", "image")
+ if logo_mode not in {"image", "gif", "dot"}:
+ logo_mode = "image"
+ animate = env_flag_true("CODE_AGENT_LOGO_ANIMATE", default=True)
+
+ def _set_logo(renderable) -> None:
+ # Rich 的居中对齐(配合 #logo 的 content-align 更稳)
+ self.query_one("#logo", Static).update(Align.center(renderable))
+
+ for p in _candidate_paths():
+ if not p.exists() or not p.is_file():
+ continue
+ try:
+ from PIL import Image, ImageSequence # type: ignore
+ except Exception:
+ break
+
+ try:
+ img = Image.open(p)
+ except Exception:
+ continue
+
+ # 以第一帧尺寸计算目标宽度(尽量保持原图尺寸,不放大)
+ try:
+ w0, h0 = img.size
+ width_chars = _fit_width_no_upscale(int(w0), int(h0), mode=logo_mode)
+ except Exception:
+ width_chars = max(24, min(72, (self.size.width or 80) - 10))
+
+ is_gif = (getattr(img, "format", "") or "").upper() == "GIF"
+ is_animated = bool(getattr(img, "is_animated", False)) or (getattr(img, "n_frames", 1) or 1) > 1
+
+ # 模式2:gif 动图(仅在 logo_mode=gif 时启用;否则一律按静态图处理)
+ if logo_mode == "gif" and animate and (is_gif or is_animated):
+ frames: list[Text] = []
+ duration_ms = int((img.info or {}).get("duration") or 90)
+ duration_ms = max(50, min(500, duration_ms))
+
+ try:
+ for frame in ImageSequence.Iterator(img):
+ rgb = frame.convert("RGB")
+ # gif 模式:默认回到“半块字符渲染”
+ t = _render_image_to_text(rgb, width=width_chars)
+ if t is not None:
+ frames.append(t)
+ if len(frames) >= 120: # 防止超长 GIF 过重
+ break
+ except Exception:
+ frames = []
+
+ if frames:
+ self._logo_frames = frames
+ _set_logo(Panel(frames[0], box=box.ROUNDED, border_style="#202637", padding=(0, 1)))
+
+ def _advance() -> None:
+ if not self._logo_frames:
+ return
+ self._logo_frame_idx = (self._logo_frame_idx + 1) % len(self._logo_frames)
+ frame_t = self._logo_frames[self._logo_frame_idx]
+ _set_logo(Panel(frame_t, box=box.ROUNDED, border_style="#202637", padding=(0, 1)))
+
+ try:
+ self._logo_timer = self.set_interval(duration_ms / 1000.0, _advance) # type: ignore[attr-defined]
+ except Exception:
+ self._logo_timer = None
+ return
+
+ # 模式1/3:静态渲染(image / dot),以及 gif 输入但非 gif 模式时
+ try:
+ rgb0 = img.convert("RGB")
+ if logo_mode == "dot":
+ t0 = _render_image_to_dots(rgb0, width_chars=width_chars)
+ else:
+ # image 模式:默认回到“半块字符渲染”
+ t0 = _render_image_to_text(rgb0, width=width_chars)
+ except Exception:
+ t0 = None
+ if t0 is not None:
+ _set_logo(Panel(t0, box=box.ROUNDED, border_style="#202637", padding=(0, 1)))
+ return
+
+ # Fallback ASCII banner (always works).
+ ascii_logo = Text(
+ "\n".join(
+ [
+ " _ _ _ _ ",
+ " | \\ | | __ _(_)_ __ __| | ___ _ __ __ _",
+ " | \\| |/ _` | | '_ \\ / _` |/ _ \\| '_ \\ / _` |",
+ " | |\\ | (_| | | | | | (_| | (_) | | | | (_| |",
+ " |_| \\_|\\__,_|_|_| |_|\\__,_|\\___/|_| |_|\\__,_|",
+ " 奶 龙 · CodeGamer ",
+ ]
+ ),
+ style="#7aa2f7",
+ )
+ _set_logo(Panel(ascii_logo, box=box.ROUNDED, border_style="#202637", padding=(0, 1)))
+
+ def _write_kv(self, key: str, value: str) -> None:
+ t = Text()
+ t.append(f"{key}: ", style="bold #7aa2f7")
+ t.append(value, style="#e8e8e8")
+ self._mount_output(Static(t))
+
+ def _write_dim(self, text: str) -> None:
+ self._write(text, style="dim")
+
+ def _write_user_message(self, user_in: str) -> None:
+ # Highlight @references inside user text.
+ t = Text()
+ for part in user_in.split(" "):
+ if part.startswith("@"):
+ t.append(part, style="bold #7aa2f7")
+ else:
+ t.append(part)
+ t.append(" ")
+ # rich.text.Text.rstrip() 是原地修改并返回 None
+ t.rstrip()
+ ts = time.strftime("%H:%M:%S")
+ self._mount_output(
+ Static(
+ Panel(
+ t,
+ title=f"😅 user · #{self.turns} · {ts}",
+ title_align="left",
+ box=box.ROUNDED,
+ border_style="#4FC3F7",
+ padding=(0, 1),
+ )
+ )
+ )
+
+ def _write_assistant_message(self, text: str) -> None:
+ ts = time.strftime("%H:%M:%S")
+ self._mount_output(
+ Static(
+ Panel(
+ Text(text or ""),
+ title=f"奶浓认为是这样的:· {ts}",
+ title_align="left",
+ box=box.ROUNDED,
+ border_style="#E94560",
+ padding=(0, 1),
+ )
+ )
+ )
+
+ def _write_success(self, text: str) -> None:
+ self._write(text, style="bold green")
+
+ def _write_warning(self, text: str) -> None:
+ self._write(text, style="bold yellow")
+
+ def _write_error(self, text: str) -> None:
+ self._write(text, style="bold red")
+
+ def _write_stream(self, chunk: str, kind: str) -> None:
+ """Write tool/agent intermediate logs with lighter weight."""
+ # Preserve blank lines
+ if chunk == "":
+ self._write("")
+ return
+
+ # During a turn: stream logs into the collapsible thought panel by default.
+ if self._thought_log is not None:
+ self._thought_log.write(Text(chunk))
+ return
+
+ # Heuristic coloring for common log prefixes.
+ style = "dim"
+ if chunk.startswith("✅") or chunk.startswith("[OK]"):
+ style = "green"
+ elif chunk.startswith("❌") or chunk.startswith("[ERROR]"):
+ style = "red"
+ elif chunk.startswith("⚠️") or chunk.startswith("[WARNING]"):
+ style = "yellow"
+ elif chunk.startswith("🧠"):
+ style = "#bb9af7"
+ elif chunk.startswith("🔍") or chunk.startswith("📷"):
+ style = "#7aa2f7"
+
+ prefix = "· " if kind == "stdout" else "‼ "
+ self._write(prefix + chunk, style=style)
+
+ def _mount_output(self, widget: Widget) -> None:
+ """Mount a widget into the scrollable output area and keep it scrolled to end."""
+ out = self.query_one("#output", VerticalScroll)
+ try:
+ out.mount(widget)
+ except Exception:
+ return
+ try:
+ out.scroll_end(animate=False)
+ except Exception:
+ pass
+
+ def _set_busy(self, busy: bool) -> None:
+ self._busy = busy
+ input_widget = self.query_one("#input_bar", Input)
+ input_widget.disabled = busy
+ prompt = self.query_one("#input_prompt", Static)
+ prompt.update("⏳" if busy else ">")
+ try:
+ header = self.query_one(Header)
+ header.sub_title = "处理中…(过程日志会实时输出)" if busy else ""
+ except Exception:
+ pass
+
+ # ============================================================
+ # Path auto-completion
+ # ============================================================
+
+ def _update_suggestions(self, text: str) -> None:
+ suggestions_view = self.query_one("#suggestions", ListView)
+ suggestions_view.clear()
+
+ # Check for command completion first (starts with /)
+ if text.startswith("/"):
+ cmd_prefix = text.lower()
+ matching_cmds = [(cmd, desc) for cmd, desc in COMMANDS if cmd.startswith(cmd_prefix)]
+
+ if matching_cmds:
+ self._suggestions = [cmd for cmd, _ in matching_cmds]
+ self._completion_start = 0
+ self._completion_tag = "/"
+ for cmd, desc in matching_cmds:
+ suggestions_view.append(SuggestionItem(cmd, desc))
+ suggestions_view.display = True
+ suggestions_view.index = 0
+ return
+
+ # Check for path completion (@)
+ tag, prefix, replace_start = self._extract_completion_context(text)
+ suggestions = self._get_path_suggestions(tag, prefix) if tag else []
+
+ if suggestions:
+ self._suggestions = suggestions
+ for item in suggestions:
+ suggestions_view.append(SuggestionItem(item))
+ suggestions_view.display = True
+ suggestions_view.index = 0
+ self._completion_start = replace_start
+ self._completion_tag = tag
+ else:
+ self._suggestions = []
+ suggestions_view.display = False
+ self._completion_start = None
+ self._completion_tag = None
+
+ def _extract_completion_context(self, text: str) -> tuple[str | None, str | None, Optional[int]]:
+ """Extract @ completion context - simplified syntax: @path instead of @file(path)."""
+ # Find the last @ that's not already completed (followed by space or end)
+ idx = text.rfind("@")
+ if idx == -1:
+ return None, None, None
+
+ # Get the part after @
+ after_at = text[idx + 1:]
+
+ # If there's a space after the path, this @ is already completed
+ # Find where the current path ends (space marks end of path)
+ space_idx = after_at.find(" ")
+ if space_idx != -1:
+ # Check if there's another @ after this one
+ rest = after_at[space_idx:]
+ next_at = rest.rfind("@")
+ if next_at != -1:
+ # Recalculate from the new @ position
+ new_idx = idx + 1 + space_idx + next_at
+ after_at = text[new_idx + 1:]
+ idx = new_idx
+ space_idx = after_at.find(" ")
+ if space_idx != -1:
+ return None, None, None
+ else:
+ return None, None, None
+
+ # The prefix is everything after @
+ prefix = after_at.strip()
+ replace_start = idx + 1
+ return "@", prefix, replace_start
+
+ def _get_path_suggestions(self, tag: str | None, prefix: str | None) -> list[str]:
+ """Get path suggestions for @ completion - shows both files and folders."""
+ if not tag or prefix is None:
+ return []
+ prefix = prefix.strip()
+ if prefix.startswith("~"):
+ prefix = os.path.expanduser(prefix)
+
+ base_dir = self.repo_root
+ needle = prefix
+ path_prefix = ""
+
+ if "/" in prefix:
+ base_part = prefix.rsplit("/", 1)[0]
+ needle = prefix.rsplit("/", 1)[1]
+ path_prefix = base_part + "/"
+ base_dir = (self.repo_root / base_part).resolve()
+
+ if not base_dir.exists() or not base_dir.is_dir():
+ return []
+
+ def iter_entries() -> Iterable[Path]:
+ try:
+ # Sort: directories first, then files, both alphabetically
+ entries = sorted(base_dir.iterdir(), key=lambda x: (x.is_file(), x.name.lower()))
+ except Exception:
+ return []
+ ignored = {".git", ".hg", ".svn", "__pycache__", "node_modules", ".venv", "venv", ".idea", ".vscode"}
+ for entry in entries:
+ if entry.name.startswith(".") and entry.name not in {".gitignore", ".env.example", ".env"}:
+ continue
+ if entry.name in ignored:
+ continue
+ yield entry
+
+ results: list[str] = []
+ for entry in iter_entries():
+ if needle and not entry.name.lower().startswith(needle.lower()):
+ continue
+ suffix = "/" if entry.is_dir() else ""
+ # Return full relative path from where user started typing
+ results.append(path_prefix + entry.name + suffix)
+ if len(results) >= 20:
+ break
+ return results
+
+ def action_complete(self) -> None:
+ """Apply first suggestion on Tab."""
+ suggestions_view = self.query_one("#suggestions", ListView)
+ if not suggestions_view.display or not self._suggestions:
+ return
+ idx = suggestions_view.index or 0
+ if 0 <= idx < len(self._suggestions):
+ self._apply_completion(self._suggestions[idx])
+
+ def action_suggestion_up(self) -> None:
+ """Move selection up in suggestions."""
+ suggestions_view = self.query_one("#suggestions", ListView)
+ if suggestions_view.display and self._suggestions:
+ current = suggestions_view.index or 0
+ suggestions_view.index = max(0, current - 1)
+
+ def action_suggestion_down(self) -> None:
+ """Move selection down in suggestions."""
+ suggestions_view = self.query_one("#suggestions", ListView)
+ if suggestions_view.display and self._suggestions:
+ current = suggestions_view.index or 0
+ suggestions_view.index = min(len(self._suggestions) - 1, current + 1)
+
+ def action_hide_suggestions(self) -> None:
+ """Hide suggestions list."""
+ suggestions_view = self.query_one("#suggestions", ListView)
+ suggestions_view.display = False
+
+ def _apply_completion(self, value: str) -> None:
+ input_widget = self.query_one("#input_bar", Input)
+ text = input_widget.value
+ if self._completion_start is None:
+ return
+ new_text = text[: self._completion_start] + value
+ input_widget.value = new_text
+ input_widget.cursor_position = len(new_text)
+ suggestions_view = self.query_one("#suggestions", ListView)
+ suggestions_view.display = False
+ self._suggestions = []
+
+ # ============================================================
+ # Event handlers
+ # ============================================================
+
+ def on_list_view_selected(self, event: ListView.Selected) -> None:
+ """Handle suggestion selection via click or Enter."""
+ if isinstance(event.item, SuggestionItem):
+ self._apply_completion(event.item.value)
+ # Refocus input
+ self.query_one("#input_bar", Input).focus()
+
+ def on_input_changed(self, event: Input.Changed) -> None:
+ if self._busy:
+ return
+ self._update_suggestions(event.value)
+
+ async def on_input_submitted(self, event: Input.Submitted) -> None:
+ # In splash mode, hide logo on first real interaction to free space.
+ if self._logo_visibility == "once":
+ self._set_logo_visible(False)
+ # First check if suggestions are visible and should be applied
+ suggestions_view = self.query_one("#suggestions", ListView)
+ if suggestions_view.display and self._suggestions:
+ idx = suggestions_view.index or 0
+ if 0 <= idx < len(self._suggestions):
+ self._apply_completion(self._suggestions[idx])
+ return
+
+ user_in = event.value.strip()
+ event.input.value = ""
+ if not user_in:
+ self._write("请提供具体指令或问题。")
+ return
+
+ # "!" 直通终端:用户自己执行命令,不走 agent
+ if user_in.startswith("!"):
+ cmd = user_in[1:].strip()
+ if not cmd:
+ self._write_warning("用法:! 例如:!pwd")
+ return
+ await self._run_bang_command(cmd, allow_dangerous=False)
+ return
+
+ if self.pending_tool_name and self.pending_tool_input is not None:
+ decision = user_in.lower()
+ tool = self.pending_tool_name
+ tool_input = self.pending_tool_input
+ original = self.pending_tool_user_input or ""
+ # clear first to avoid re-entrancy
+ self.pending_tool_name = None
+ self.pending_tool_input = None
+ self.pending_tool_user_input = None
+ if decision in {"y", "yes"}:
+ # run tool, then feed result back to agent for next decision
+ await self._run_tool_then_continue(tool, tool_input, original, approved=True)
+ else:
+ await self._run_tool_then_continue(tool, tool_input, original, approved=False)
+ return
+
+ # "! 命令" 的危险确认(只针对 bang 模式,不走 agent)
+ if self.pending_bang_command:
+ decision = user_in.lower()
+ cmd = self.pending_bang_command
+ self.pending_bang_command = None
+ if decision in {"y", "yes"}:
+ await self._run_bang_command(cmd, allow_dangerous=True)
+ else:
+ self._write_warning("已取消执行该命令。")
+ return
+
+ if self.pending_patch_text:
+ if user_in.lower() in {"y", "yes"}:
+ self._apply_patch(self.pending_user_input or "", self.pending_patch_text)
+ else:
+ self._write("已取消补丁应用。")
+ # Feed back to agent so it can decide next step without manual re-typing.
+ reject_prompt = (
+ "用户拒绝应用你生成的补丁。请解释原因/提供替代方案,或生成更小、更安全的补丁。"
+ )
+ self.turns += 1
+ self._write("")
+ self._write_user_message("(系统)用户拒绝应用补丁,继续决策")
+ await self._run_turn_async(reject_prompt)
+ self.pending_patch_text = None
+ self.pending_user_input = None
+ return
+
+ if user_in in {"/q", "/quit", "quit", "exit"}:
+ self._write("")
+ self._write("没钱充token了,下次再见")
+ self.exit()
+ return
+
+ if user_in.startswith("/stats"):
+ self._handle_stats(user_in)
+ return
+
+ if user_in.startswith("/export"):
+ self._handle_export(user_in)
+ return
+
+ # /verify 和 /fix 命令已移除,不再处理
+
+ if user_in.startswith("/plan"):
+ self._handle_plan(user_in)
+ return
+
+ if user_in.startswith("/model"):
+ self._handle_model(user_in)
+ return
+
+ if user_in == "/clear":
+ out = self.query_one("#output", VerticalScroll)
+ # Remove all children
+ for child in list(out.children):
+ try:
+ child.remove()
+ except Exception:
+ pass
+ return
+
+ self.turns += 1
+ self._write("")
+ self._write_user_message(user_in)
+ await self._run_turn_async(user_in)
+
+ # ============================================================
+ # Command handlers
+ # ============================================================
+
+ def _handle_plan(self, user_in: str) -> None:
+ raw = user_in[len("/plan") :].strip()
+ save_plan = False
+ if "--save" in raw:
+ save_plan = True
+ raw = raw.replace("--save", "").strip()
+ goal = raw or "请为当前任务生成一个可执行计划"
+ response = self.agent.registry.execute_tool("plan", goal)
+ self._write("")
+ self._write("🤖 plan")
+ self._write(response)
+ if save_plan:
+ self.agent.note_tool.run(
+ {
+ "action": "create",
+ "title": "Plan",
+ "content": f"Goal:\n{goal}\n\nPlan:\n\n{response}",
+ "note_type": "plan",
+ "tags": [self.project, "plan"],
+ }
+ )
+ self._write("✅ 已保存到 notes")
+
+ def _handle_model(self, user_in: str) -> None:
+ arg = user_in[len("/model") :].strip()
+ model_list = list(AVAILABLE_MODELS.items())
+ if not arg:
+ self._write("")
+ model_type = "多模态" if self.llm.is_multimodal else "文本"
+ self._write(f"当前模型: {self.llm.model} ({model_type})")
+ self._write("")
+ self._write("可用模型:")
+ for i, (name, info) in enumerate(model_list, 1):
+ mtype = "多模态" if info["multimodal"] else "文本"
+ marker = "-> " if name == self.llm.model else " "
+ self._write(f" {marker}[{i}] {name} [{mtype}]")
+ self._write("")
+ self._write("用法:/model <序号或模型名>")
+ return
+
+ target_model: Optional[str] = None
+ if arg.isdigit():
+ idx = int(arg) - 1
+ if 0 <= idx < len(model_list):
+ target_model = model_list[idx][0]
+ elif arg in AVAILABLE_MODELS:
+ target_model = arg
+
+ if not target_model:
+ self._write("未知模型,请输入序号或模型名。")
+ return
+
+ self.llm.switch_model(target_model)
+ model_type = "多模态" if self.llm.is_multimodal else "文本"
+ self._write(f"✓ 已切换到: {target_model} ({model_type})")
+
+ def _handle_stats(self, user_in: str) -> None:
+ arg = user_in[len("/stats") :].strip()
+ log_dir = env_str("CODE_AGENT_LOG_DIR") or str(Path(".helloagents") / "logs")
+ log_path = Path(log_dir) / "events.jsonl"
+ events = load_events(log_path)
+ if not events:
+ self._write("暂无日志数据。")
+ return
+
+ current_id = env_str("CODE_AGENT_SESSION_ID")
+ target_id = None
+ if arg == "current" or not arg:
+ target_id = current_id
+ elif arg == "last":
+ for e in reversed(events):
+ if e.get("type") == "session_end":
+ target_id = e.get("session_id")
+ break
+ else:
+ target_id = arg
+
+ if not target_id:
+ self._write("未找到目标会话。")
+ return
+
+ session_events = [e for e in events if e.get("session_id") == target_id]
+ if not session_events:
+ self._write(f"未找到会话: {target_id}")
+ return
+
+ stats = summarize_session(session_events)
+ self._write("")
+ self._write("📊 会话统计")
+ self._write(f"session_id: {target_id}")
+ if stats["start_ts"]:
+ self._write(f"start: {stats['start_ts']}")
+ if stats["end_ts"]:
+ self._write(f"end: {stats['end_ts']}")
+ if stats["duration_ms"] is not None:
+ self._write(f"duration: {stats['duration_ms']} ms")
+ self._write(f"turns: {stats['turns']}")
+ self._write(f"tool_calls: {stats['tool_calls']} (errors: {stats['tool_errors']})")
+ self._write(f"llm_calls: {stats['llm_calls']} (errors: {stats['llm_errors']})")
+ if stats["prompt_tokens"] or stats["completion_tokens"]:
+ self._write(f"tokens: prompt={stats['prompt_tokens']} completion={stats['completion_tokens']}")
+ self._write(f"tokens_est: prompt≈{stats['prompt_tokens_est']} completion≈{stats['completion_tokens_est']}")
+
+ def _handle_export(self, user_in: str) -> None:
+ arg = user_in[len("/export") :].strip()
+ log_dir = env_str("CODE_AGENT_LOG_DIR") or str(Path(".helloagents") / "logs")
+ log_path = Path(log_dir) / "events.jsonl"
+ events = load_events(log_path)
+ if not events:
+ self._write("暂无日志数据。")
+ return
+
+ current_id = env_str("CODE_AGENT_SESSION_ID")
+ target_id = None
+ if arg == "current" or not arg:
+ target_id = current_id
+ elif arg == "last":
+ for e in reversed(events):
+ if e.get("type") == "session_end":
+ target_id = e.get("session_id")
+ break
+ else:
+ target_id = arg
+
+ if not target_id:
+ self._write("未找到目标会话。")
+ return
+
+ session_events = [e for e in events if e.get("session_id") == target_id]
+ if not session_events:
+ self._write(f"未找到会话: {target_id}")
+ return
+
+ export_dir = Path(log_dir).parent / "exports"
+ export_path = export_session(target_id, session_events, export_dir)
+ self._write("✅ 已导出会话信息")
+ self._write(f"path: {export_path}")
+
+ # ============================================================
+ # Agent interaction
+ # ============================================================
+
+ async def _run_turn_async(self, user_in: str) -> None:
+ """Run one agent turn without blocking the UI.
+
+ - The agent runs in a background thread.
+ - stdout/stderr are streamed into the output panel.
+ - At the end, we render the assistant final message in a readable panel.
+ """
+ self._set_busy(True)
+
+ # Create a collapsible "thinking" panel for this turn (collapsed by default).
+ thought_log = RichLog(wrap=True, markup=True)
+ self._thought_log = thought_log
+ self._mount_output(
+ Collapsible(
+ thought_log,
+ title="奶浓思考ing...",
+ collapsed=True,
+ collapsed_symbol="▶",
+ expanded_symbol="▼",
+ )
+ )
+ stream_out = _StreamingTUIWriter(self, kind="stdout")
+ stream_err = _StreamingTUIWriter(self, kind="stderr")
+
+ def _run() -> str:
+ with contextlib.redirect_stdout(stream_out), contextlib.redirect_stderr(stream_err):
+ return self.agent.run_turn(user_in)
+
+ try:
+ response = await asyncio.to_thread(_run)
+ except FileNotFoundError as e:
+ stream_out.finish()
+ stream_err.finish()
+ self._write_error(f"文件不存在:{e}")
+ self._write_dim("提示:使用 @ 引用文件/目录,例如 @main.py @src/ 请分析")
+ return
+ except HelloAgentsException as e:
+ stream_out.finish()
+ stream_err.finish()
+ self._write_error(f"LLM 调用失败: {e}")
+ return
+ finally:
+ # Flush any remaining partial output.
+ try:
+ stream_out.finish()
+ stream_err.finish()
+ except Exception:
+ pass
+ # Stop routing stream output into thought log
+ self._thought_log = None
+ self._set_busy(False)
+
+ # Render the assistant's final answer (high-contrast, easy to scan).
+ self._write("")
+ self._write_assistant_message(response)
+
+ # Tool confirmation marker interception (Cursor-like gating)
+ m = CONFIRM_TOOL_RE.search(response or "")
+ if m:
+ try:
+ payload = json.loads(m.group(1))
+ tool = payload.get("tool") or "terminal"
+ tool_input = payload.get("tool_input") or ""
+ # store pending tool request
+ self.pending_tool_name = str(tool)
+ self.pending_tool_input = str(tool_input)
+ self.pending_tool_user_input = user_in
+
+ self._write("")
+ self._write_rule("需要确认执行命令/工具", border_style="#e0af68", title_style="bold #e0af68")
+ self._write_kv("tool", str(tool))
+ self._write_kv("input", str(tool_input)[:400])
+ self._write_warning("是否允许运行?(y/n)")
+ except Exception:
+ pass
+ return
+
+ patch_text = extract_patch(response)
+ if not patch_text:
+ return
+ patch_text = normalize_patch(patch_text)
+ if patch_text.strip() == "*** Begin Patch\n*** End Patch":
+ return
+
+ # Cursor-like gating: always require confirmation before applying patch.
+ self.pending_patch_text = patch_text
+ self.pending_user_input = user_in
+ self._write("")
+ self._write_rule("即将应用补丁", border_style="#e0af68", title_style="bold #e0af68")
+ # stats (parse blocks so Add File shows meaningful +lines)
+ lines = patch_text.splitlines()
+ files: list[str] = []
+ created = updated = deleted = 0
+ add = sub = 0
+
+ current_op: str | None = None # "add" | "update" | "delete"
+ in_hunk = False
+ for l in lines:
+ if l.startswith("*** Add File:"):
+ files.append(l.replace("*** ", "").strip())
+ created += 1
+ current_op = "add"
+ in_hunk = False
+ continue
+ if l.startswith("*** Update File:"):
+ files.append(l.replace("*** ", "").strip())
+ updated += 1
+ current_op = "update"
+ in_hunk = False
+ continue
+ if l.startswith("*** Delete File:"):
+ files.append(l.replace("*** ", "").strip())
+ deleted += 1
+ current_op = "delete"
+ in_hunk = False
+ continue
+ if l.startswith("@@"):
+ in_hunk = True
+ continue
+ if l.strip() == "*** End Patch":
+ break
+
+ # Count changes
+ if current_op == "add":
+ # Add File blocks should be treated as additions even if the model forgot '+' prefixes.
+ if l.startswith("+") and not l.startswith("+++"):
+ add += 1
+ elif l.startswith(("***", "@@")):
+ continue
+ else:
+ # Non-empty content line counts as an added line
+ if l != "":
+ add += 1
+ continue
+
+ if current_op == "update" and in_hunk:
+ if l.startswith("+") and not l.startswith("+++"):
+ add += 1
+ elif l.startswith("-") and not l.startswith("---"):
+ sub += 1
+ continue
+
+ if files:
+ self._write_kv("files", ", ".join(files[:8]) + (" ..." if len(files) > 8 else ""))
+ self._write_kv("ops", f"add={created} update={updated} delete={deleted}")
+ self._write_kv("diff", f"+{add} / -{sub}")
+ self._write_warning("是否应用?(y/n)")
+ return
+
+ async def _run_tool_then_continue(self, tool: str, tool_input: str, original_user_in: str, approved: bool) -> None:
+ """Run a pending tool (if approved) and continue agent decision."""
+ self._set_busy(True)
+
+ if not approved:
+ self._write_warning(f"已拒绝执行:{tool}")
+ prompt = (
+ f"用户拒绝执行工具/命令:{tool}\n"
+ f"拟执行输入:{tool_input}\n\n"
+ "请基于现有信息继续决策(不要再要求执行同一命令),给出替代取证方式或直接结论。"
+ )
+ self._set_busy(False)
+ self.turns += 1
+ self._write("")
+ self._write_user_message("(系统)拒绝执行命令后继续")
+ await self._run_turn_async(prompt)
+ return
+
+ # approved: execute tool and feed result back to agent
+ # 对齐 Claude Code/OpenCode:用户点 y 后,这次执行应当真正放行(而不是被工具内部硬白名单再拒绝)
+ # 我们通过向 terminal 工具注入 `user_approved=true` 来实现“本次/一次性 token”。
+ effective_input = tool_input
+ if tool == "terminal":
+ try:
+ obj = json.loads(tool_input)
+ if isinstance(obj, dict):
+ obj["user_approved"] = True
+ # 用户已在 UI 二次确认:允许 terminal 将该次执行视为已授权(等价一次性 token)
+ effective_input = json.dumps(obj, ensure_ascii=False)
+ except Exception:
+ # 非结构化输入保持原样
+ effective_input = tool_input
+
+ def _run() -> str:
+ return self.agent.registry.execute_tool(tool, effective_input)
+
+ try:
+ out = await asyncio.to_thread(_run)
+ self._write_success(f"已执行:{tool}")
+ self._write(out)
+ prompt = (
+ f"用户允许执行工具/命令:{tool}\n"
+ f"输入:{effective_input}\n\n"
+ f"输出:\n{out}\n\n"
+ "请基于该输出继续下一步(不要重复执行同一命令)。"
+ )
+ finally:
+ self._set_busy(False)
+
+ self.turns += 1
+ self._write("")
+ self._write_user_message("(系统)命令结果已获取,继续决策")
+ await self._run_turn_async(prompt)
+
+
+ async def _run_bang_command(self, command: str, *, allow_dangerous: bool) -> None:
+ """Run a user-requested shell command directly (no agent)."""
+ self._write("")
+ self._write_rule("用户终端(!)", border_style="#4FC3F7", title_style="bold #4FC3F7")
+ self._write_kv("command", command)
+
+ self._set_busy(True)
+
+ def _run() -> str:
+ payload = json.dumps(
+ {"command": command, "allow_dangerous": allow_dangerous, "shell_mode": True},
+ ensure_ascii=False,
+ )
+ return self.agent.registry.execute_tool("terminal", payload)
+
+ try:
+ out = await asyncio.to_thread(_run)
+ finally:
+ self._set_busy(False)
+
+ # If terminal asks for dangerous confirmation, do it in UI instead of blocking stdin.
+ if isinstance(out, str) and "allow_dangerous=true" in out and not allow_dangerous:
+ self.pending_bang_command = command
+ self._write_warning("该命令需要确认(可能包含写盘/命令替换/非白名单)。是否继续?(y/n)")
+ return
+
+ # Show result
+ self._write(out if isinstance(out, str) else str(out))
+
+ def _apply_patch(self, user_in: str, patch_text: str) -> None:
+ try:
+ start = time.time()
+ res = self.patch_executor.apply(patch_text)
+ self._write("")
+ self._write("✅ Patch applied")
+ self._write(f"files: {', '.join(res.files_changed) if res.files_changed else '(none)'}")
+ if res.backups:
+ self._write(f"backups: {len(res.backups)} (in .helloagents/backups/...)")
+ log_event(
+ "patch_apply",
+ {
+ "ok": True,
+ "ms": int((time.time() - start) * 1000),
+ "summary": f"{len(res.files_changed or [])} files",
+ "files_changed": res.files_changed,
+ },
+ )
+ self.agent.note_tool.run(
+ {
+ "action": "create",
+ "title": "Patch applied",
+ "content": f"User input:\n{user_in}\n\nPatch:\n\n```text\n{patch_text}\n```\n\nFiles:\n"
+ + "\n".join([f"- {p}" for p in res.files_changed]),
+ "note_type": "action",
+ "tags": [self.project, "patch_applied"],
+ }
+ )
+ except PatchApplyError as e:
+ self._write("")
+ self._write(f"❌ Patch failed: {e}")
+ log_event(
+ "patch_apply",
+ {
+ "ok": False,
+ "ms": 0,
+ "summary": "PatchApplyError",
+ "error": str(e),
+ },
+ )
+ self.agent.note_tool.run(
+ {
+ "action": "create",
+ "title": "Patch failed",
+ "content": f"Error: {e}\n\nUser input:\n{user_in}\n\nPatch:\n\n```text\n{patch_text}\n```\n",
+ "note_type": "blocker",
+ "tags": [self.project, "patch_failed"],
+ }
+ )
+
+
+# ============================================================
+# Entry point
+# ============================================================
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(description="HelloAgents Code Agent TUI")
+ parser.add_argument("--repo", type=str, default=".", help="Repository root (workspace). Default: .")
+ parser.add_argument("--project", type=str, default=None, help="Project name (default: repo folder name)")
+ args = parser.parse_args(argv)
+
+ repo_root = Path(args.repo).resolve()
+ load_dotenv(dotenv_path=repo_root / ".env", override=False)
+
+ app = CodeAgentTUI(repo_root=repo_root, project=args.project)
+ app.run()
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/prompts/README.md b/Co-creation-projects/aug618-Praxis/code_agent/prompts/README.md
new file mode 100644
index 00000000..803acfdf
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/prompts/README.md
@@ -0,0 +1,38 @@
+# Prompts
+
+本目录用于集中管理 Code Agent 的提示词,方便独立迭代与对比。
+
+## 文件说明
+- `system.md`:全局行为与安全边界(按需探索/敏感操作确认/补丁格式)
+- `react.md`:ReAct 回合格式与工具输入约定
+- `plan.md`:规划工具(`plan[...]`)专用提示词
+- `summarize_observation.md`:工具输出摘要提示词
+
+## 核心设计理念(类似 Claude Code)
+
+### 按需探索
+- **保底上下文**(自动注入):系统提示 + 对话历史[-10:] + 上次工具摘要[-3:]
+- **扩展上下文**(按需获取):通过 `context_fetch` 工具由模型主动调用
+
+### context_fetch 工具使用指南
+
+**何时使用:**
+- ✅ 需要搜索代码中的类/函数定义
+- ✅ 用户问"有没有关于 X 的笔记/记忆"
+- ✅ 提到错误栈/报错信息,需要找相关代码
+- ❌ 用户问"我们刚才说了什么"(直接用对话历史)
+- ❌ 已经通过 terminal 拿到足够证据
+
+**参数说明:**
+```json
+{
+ "sources": ["files", "notes", "memory", "tests"], // 可多选
+ "query": "ContextBuilder", // 关键词
+ "paths": "context/**/*.py" // 可选,限定范围
+}
+```
+
+**预算控制:** 每个数据源返回最多 ~800 tokens,自动截断,支持缓存
+
+**调用策略:** 先用保底上下文推理,证据不足再调用;避免盲目全局扫描
+
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/prompts/plan.md b/Co-creation-projects/aug618-Praxis/code_agent/prompts/plan.md
new file mode 100644
index 00000000..bba880d0
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/prompts/plan.md
@@ -0,0 +1,18 @@
+你是一个“可选规划工具”。只有当需要多步执行、或用户强制要求计划时才会调用你。
+
+请输出一个可执行计划,要求:
+- 5~12 条步骤为宜,按可运行顺序排列
+- 每步尽量具体到“要看哪些文件/用什么命令/产出什么”
+- 若涉及修改代码:明确“先检索证据 -> 再给 patch -> 需要用户确认才落盘”
+
+输出格式(Markdown):
+## Plan
+1. ...
+2. ...
+
+## Risks
+- ...
+
+## Validation
+- ...
+
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/prompts/react.md b/Co-creation-projects/aug618-Praxis/code_agent/prompts/react.md
new file mode 100644
index 00000000..8a70d5d3
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/prompts/react.md
@@ -0,0 +1,122 @@
+你是一个具备推理与行动能力的 Code Agent。你可以思考,然后调用工具获取证据,最终给出结论或补丁。
+
+## 可用工具
+{tools}
+
+## 工作流程(严格遵守)
+**每次回复必须包含 Thought 和 Action 两部分,缺一不可:**
+
+Thought: <你的思考(简短)>
+Action: <以下二选一>
+- tool_name[tool_input]
+- Finish[最终回答(必要时包含 *** Begin Patch...*** End Patch)]
+
+**关键规则:**
+1. **永远不要只有 Thought 没有 Action** - 这会导致解析失败!
+2. 如果已有足够信息回答,必须用 `Finish[答案]` 结束
+3. 每次只执行一个工具调用,等待结果后再决定下一步
+4. 不要连续写多个 Action
+## 证据与任务管理策略(重要)
+**优先使用保底上下文(对话历史 + 上次工具结果)推理,证据不足时再调用工具:**
+
+1. **先评估已有信息**:检查对话历史和上次工具输出是否已包含答案
+2. **需要新证据时**:
+ - 涉及"之前说了什么/记得吗" → 直接查看对话历史,不需要调用工具
+ - 需要查看代码/文件 → 优先 `terminal` (快速定位)
+ - 需要搜索代码/笔记/记忆 → 使用 `context_fetch` (聚合搜索,单次上限 ~800 tokens/源)
+ - 需要执行命令/写笔记 → 使用对应工具
+3. **多步骤/需持续跟踪的任务**:如果任务有≥2个子步骤、需用户确认、或跨回合继续,请先/及时用 `todo` 记录或更新;确保同时最多 1 个 `in_progress`。若用户表达“分步/步骤/三步/改造/计划/完成后”等,多数情况下先 `todo add` 再行动,结尾 `todo list` 汇总。
+4. **避免过度收集**:不要为了"更全面"而反复调用工具
+
+## context_fetch 使用指南
+何时使用:
+- ✅ 用户问"有没有关于 X 的笔记/记忆"
+- ✅ 需要搜索代码中的类/函数定义
+- ✅ 提到错误栈/报错信息,需要找相关代码
+- ❌ 用户问"我们刚才说了什么" (直接用对话历史)
+- ❌ 已经通过 terminal 拿到足够证据
+
+参数说明:
+- `sources`: 可选 ["notes", "memory", "files", "tests"],可多选
+- `query`: 关键词(类名/函数名/错误关键字)
+- `paths`: 限定搜索范围(如 "src/**/*.py"),避免全仓库扫描
+- `budget_tokens`: 单个源的返回上限,默认 800(已内置控制,不需指定)
+## 停止条件(非常重要)
+- 一旦你已经拿到了足够的证据(例如:rg 命中、关键文件片段、错误栈、配置项),**必须**使用 `Finish[...]` 结束,不要为了"更全面"继续调用更多工具。
+- 如果你发现自己准备重复执行同一个工具调用(相同命令/相同文件范围),通常说明没有新信息:**立即**改用 `Finish[...]` 给出当前结论 + 下一步最小化建议。
+- **记住:有答案就 Finish,永远不要只写 Thought 而不写 Action!**
+
+## 工具输入约定
+- terminal:推荐 JSON,例如 `terminal[{{"command":"rg -n \\"ContextBuilder\\" -S .","allow_dangerous":false}}]`
+ - 支持管道等 shell 写法(例如 `rg ... | head`)
+ - 包含重定向(`>`/`>>`)、子命令替换(`$()`/反引号)或危险命令时需确认
+- context_fetch:**聚合搜索工具(优先推荐)**,例如 `context_fetch[{{"sources":["files","notes"],"query":"ContextBuilder","paths":"context/**/*.py"}}]`
+ - 一次调用可搜索多个源(notes/memory/files/tests)
+ - 返回结构化结果,自动控制 token 预算(~800/源)
+ - **优于直接用 note/memory search:避免多次工具调用**
+- note:必须 JSON,例如 `note[{{"action":"create","title":"...","content":"...","note_type":"task_state","tags":["..."]}}]`
+- memory:推荐 JSON,例如 `memory[{{"action":"add","memory_type":"episodic","content":"...","importance":0.6}}]`
+- plan:可用纯文本目标,或 JSON(见工具说明)
+- todo:JSON 调用管理待办,适用于多步骤任务跟踪;示例
+ - `todo[{{"action":"add","title":"修复 hello 页面样式","desc":"补充内联 CSS","status":"pending"}}]`
+ - `todo[{{"action":"update","id":3,"status":"in_progress"}}]`(同时仅允许 1 个 in_progress)
+ - `todo[{{"action":"list"}}]`(输出按 in_progress/pending/completed 分组的要点列表)
+## 补丁格式(产出代码修改时)
+当需要修改代码时,在 `Finish[...]` 中输出补丁。**补丁必须单独成段,`*** Begin Patch` 必须独占一行(前面不能有任何文字)**:
+
+**正确格式:**
+```
+Finish[
+已为 testDemo/hello.html 添加样式。
+
+*** Begin Patch
+*** Update File: testDemo/hello.html
+
+
+
+
+
+
+ Hello World
+
+
+*** End Patch
+]
+```
+
+**关键要点:**
+1. 说明文字和补丁之间要有**空行**分隔
+2. `*** Begin Patch` **独占一行**(不要在同一行前面加任何字符)
+3. `*** End Patch` **独占一行**(不要在同一行后面加任何字符)
+4. 不要用 markdown 代码块包裹补丁(不要用 ``` )
+
+**常见错误对比:**
+```
+❌ 错误1:补丁前有冒号
+Finish[补丁如下:*** Begin Patch...]
+
+❌ 错误2:补丁前有文字在同一行
+Finish[这是补丁 *** Begin Patch...]
+
+❌ 错误3:没有空行分隔
+Finish[已添加样式
+*** Begin Patch...]
+
+✅ 正确:说明和补丁分段
+Finish[已添加样式
+
+*** Begin Patch...]
+```
+## 关键行为准则
+- 先证据后结论:回答“项目结构/模块职责”等问题前,先用 terminal 取到目录/文件列表/关键入口文件证据
+- 不要擅自做代码质量评审:除非用户明确要求“代码质量/重构/修 bug”
+- 不要在没有明确需求时输出补丁;需要澄清就问
+- 删除文件/大改动:先解释风险并征求确认;确认后再在 Finish 里给出补丁
+
+## 当前任务
+Question: {question}
+
+## 执行历史
+{history}
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/prompts/summarize_observation.md b/Co-creation-projects/aug618-Praxis/code_agent/prompts/summarize_observation.md
new file mode 100644
index 00000000..c2b27fc0
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/prompts/summarize_observation.md
@@ -0,0 +1,19 @@
+你是一个“工具输出摘要器”。你的任务是把一段工具输出压缩成短摘要,供后续上下文使用。
+
+要求:
+- 保留对当前任务最重要的信息(文件路径、函数/类名、报错、关键数字、结论)
+- 如果是终端命令输出:优先保留“命中行/关键信息”,不要堆满无关行
+- 不要编造不存在的内容;不确定就写“不确定/需要进一步检索”
+- 输出尽量短:目标 120~200 中文字或 10~15 行以内
+- 最后给出 1~3 条“下一步建议”(如果有)
+
+输出格式(Markdown):
+## Summary
+- ...
+
+## Key Details
+- ...
+
+## Next
+- ...
+
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/prompts/system.md b/Co-creation-projects/aug618-Praxis/code_agent/prompts/system.md
new file mode 100644
index 00000000..f7725af3
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/prompts/system.md
@@ -0,0 +1,79 @@
+你是一个“在仓库内工作的 CLI 编程助手”(类似 Claude Code/Codex),不是闲聊机器人。
+
+工作区固定为仓库根目录 `.`,核心准则:
+- 边界:所有路径必须在 repo_root 内,resolve 后校验前缀,拒绝逃逸。
+- 按需探索:只有确实需要证据时才调用终端;优先小范围命令(`ls` / `rg --files` / `rg ` / `sed -n p ` / `cat `);避免无端全库扫描。
+- 写盘唯一通道:补丁 + apply_patch。**严禁** `cat >` / `tee` / Here-Doc / 重定向等终端写法。
+- 高风险(删除/覆盖大量/危险命令 rm/chmod/git reset --hard)必须说明风险并征求确认;最终执行由 CLI 裁决。
+- 复杂任务遵循“计划 → 取证 → 补丁 → 确认 → 落盘 → 验证”节奏,最小改动满足需求。
+
+**关于对话历史的重要说明**:
+- 本段 [Role & Policies] 是你的**系统角色定义和工作规则**,不是用户对话内容
+- 当用户询问"我们之前聊了什么 / 说了什么 / 总结对话"时,请**只总结 [Context] 区块中的对话历史**(格式为 `[user]` 和 `[assistant]` 的交互记录)
+- **不要把系统规则、工具定义、角色描述当作"对话内容"来总结**
+- 总结对话时直接根据 [Context] 回答,不需要调用 memory 或 note 工具
+
+可用工具(ReAct Action 用):
+
+**优先使用的聚合搜索工具:**
+- **context_fetch[...]**:按需获取扩展上下文(单次可查多源,自动控制预算 ~800 tokens/源)
+ - JSON 格式:`{"sources": ["files","notes","memory","tests"], "query": "关键词", "paths": "src/**/*.py"}`
+ - **使用策略:先用保底上下文(对话历史+上次工具结果)推理,证据不足再调用**
+ - 优于单独调用 note/memory search:一次调用可搜索多个数据源,避免多次工具调用
+ - 自动预算控制:每源返回 ~800 tokens,避免上下文爆炸
+
+**其他工具:**
+- terminal[...]:只读检索;支持管道等,但重定向/子命令替换/危险命令需确认。写文件用补丁。
+- note[...]:记录关键结论/阻塞/行动。
+- memory[...]:跨会话情景记忆,需显式 add。
+- skills[...]:技能库(SOP/工作流)渐进式加载。先 skills[list/search] 找到合适技能,再 skills[show] 加载 SKILL.md 内容按流程执行。
+- plan[...]:多步/模糊任务时生成计划;或用户要求时调用。
+- todo[...]:多步骤任务跟踪,状态 pending/in_progress/completed(仅 1 个 in_progress)。
+
+详尽用法参考 tools.md(按需自行查阅)。
+## 补丁格式(重要)
+
+产出补丁时,必须严格遵守以下格式:
+
+```
+*** Begin Patch
+*** Add File: path/to/new_file.py
+文件内容...
+可以多行...
+*** Update File: path/to/existing_file.py
+更新后的完整文件内容...
+*** Delete File: path/to/old_file.py
+*** End Patch
+```
+
+**关键规则:**
+1. 第一行必须是 `*** Begin Patch`(前面不要有任何文字)
+2. 最后一行必须是 `*** End Patch`
+3. 操作行格式:`*** Add File: ` / `*** Update File: ` / `*** Delete File: `
+4. Add/Update 后面跟完整文件内容,Delete 后面不需要内容
+5. 不要在补丁外包裹 markdown 代码块(不要用 ```)
+6. 路径相对于仓库根目录
+
+**错误示例:**
+```
+这是一个补丁:
+*** Begin Patch
+...
+```
+❌ 问题:`*** Begin Patch` 前面有文字
+
+**正确示例:**
+```
+*** Begin Patch
+*** Add File: testDemo/style.css
+body {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ font-family: Arial, sans-serif;
+}
+*** End Patch
+```
+✅ 第一行就是 `*** Begin Patch`
+
+用户如果只是问候/闲聊,直接自然回复,不要调用工具,不要输出补丁。
+
+**输出风格**:非代码/非工具回复尽量 ≤4 行,直接给结论,避免“Here is...”等冗余开场;除非用户要求,不使用 emoji;事实性问题直接给结果。
diff --git a/Co-creation-projects/aug618-Praxis/code_agent/prompts/tools.md b/Co-creation-projects/aug618-Praxis/code_agent/prompts/tools.md
new file mode 100644
index 00000000..170b95ca
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/code_agent/prompts/tools.md
@@ -0,0 +1,50 @@
+# 工具使用指南(Claude Code 风格精简版)
+
+针对当前内置工具:`terminal`、`context_fetch`、`todo`、`note`、`memory`、`plan`。遵循“明确何时用 / 何时不用 / 如何用”,避免盲目调用。
+
+## terminal
+用途:只读检索与快速查看(ls/rg/cat/sed/head/tail/grep/git status/diff)。
+- 何时用:定位文件/符号/报错;小范围查看片段;确认目录结构。
+- 何时不用:写文件(用补丁);大范围全库扫描(除非用户要求);危险命令(rm/chmod/git reset --hard)。
+- 调用示例:`terminal[{"command":"rg -n \"foo\" context/**/*.py","allow_dangerous":false}]`
+
+## context_fetch
+用途:聚合搜索(files/notes/memory/tests),自动摘要,控制预算。
+- 何时用:需要“更多证据”时;搜类名/函数名/错误栈;需要相关笔记/记忆;比单独 note/memory 搜索更省步数。
+- 何时不用:已经有足够证据;用户仅问对话历史。
+- 调用示例:`context_fetch[{"sources":["files","notes"],"query":"ContextBuilder","paths":"context/**/*.py"}]`
+
+## todo
+用途:多步骤任务跟踪。状态:pending | in_progress(仅 1 个)| completed。
+- 何时用:3 步以上或多文件/多特性;用户列出多项需求;跨回合/需确认的任务;开始工作前先标记 in_progress,完成后立即 completed。
+- 何时不用:单一步、琐碎或纯问答。
+- 示例:
+ - `todo[{"action":"add","title":"设计简介页布局","desc":"头部/简介/技能","status":"pending"}]`
+ - `todo[{"action":"update","id":1,"status":"in_progress"}]`
+ - `todo[{"action":"update","id":1,"status":"completed"}]`
+ - `todo[{"action":"list"}]`
+
+## note
+用途:结构化笔记(action/decision/blocker/task_state 等),Markdown 持久化。
+- 何时用:记录关键结论/风险/阻塞;补丁成功/失败总结;阶段小结。
+- 何时不用:临时想法可先留在对话,不必频繁写笔记。
+- 示例:`note[{"action":"create","title":"Patch applied","content":"...","note_type":"action","tags":["patch"]}]`
+
+## memory
+用途:情景记忆(SQLite);跨会话回忆“发生过什么”。默认不开自动写,需要显式添加。
+- 何时用:需要在未来回忆本次决策/阻塞/结论;会话结束前写一条小结;复用过往经验时可先 search。
+- 何时不用:即时对话短期内容已有 history;信息尚不确定。
+- 示例:
+ - `memory[{"action":"add","memory_type":"episodic","content":"完成 hello.html 样式改造,见补丁...","importance":0.7}]`
+ - `memory[{"action":"search","query":"hello.html 样式","memory_types":["episodic"],"limit":5}]`
+
+## plan
+用途:显式规划工具,生成分步计划。
+- 何时用:任务模糊或明显多步骤;用户要求出计划;执行前需要拆解。
+- 何时不用:非常简单的一步任务。
+- 示例:`:plan 添加 dark mode 开关` 或 `plan[{"goal":"优化渲染性能,先梳理瓶颈再改"}]`
+
+## 重要提醒
+- 写/改文件必须用补丁(*** Begin Patch ...),禁止 `cat > file` / Here-Doc / tee / 重定向写盘。
+- 先用已有上下文推理,不足再调用工具;避免无端多次搜索。
+- todo 只保持 1 个 in_progress;完成立刻标记完成;阻塞则新增一条说明阻塞。
diff --git a/Co-creation-projects/aug618-Praxis/context/__init__.py b/Co-creation-projects/aug618-Praxis/context/__init__.py
new file mode 100644
index 00000000..30ec75f4
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/context/__init__.py
@@ -0,0 +1,10 @@
+"""上下文工程模块
+
+为HelloAgents框架提供上下文工程能力:
+- ContextBuilder: GSSC流水线(Gather-Select-Structure-Compress)
+"""
+
+from .builder import ContextBuilder, ContextConfig, ContextPacket
+
+__all__ = ["ContextBuilder", "ContextConfig", "ContextPacket"]
+
diff --git a/Co-creation-projects/aug618-Praxis/context/builder.py b/Co-creation-projects/aug618-Praxis/context/builder.py
new file mode 100644
index 00000000..70960a34
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/context/builder.py
@@ -0,0 +1,521 @@
+"""ContextBuilder - GSSC流水线实现
+
+实现 Gather-Select-Structure-Compress 上下文构建流程:
+1. Gather: 从多源收集候选信息(历史、记忆、RAG、工具结果)
+2. Select: 基于优先级、相关性、多样性筛选
+3. Structure: 组织成结构化上下文模板
+4. Compress: 在预算内压缩与规范化
+"""
+
+from typing import Dict, Any, List, Optional, Tuple, TYPE_CHECKING, Any as TypingAny
+from dataclasses import dataclass, field
+from datetime import datetime
+import tiktoken
+from utils.observability import log_event
+import math
+
+from core.message import Message
+from core.llm import HelloAgentsLLM
+
+if TYPE_CHECKING:
+ # Optional, only for type checking. Importing tools at runtime may pull in heavy optional deps.
+ from tools import MemoryTool, RAGTool
+else:
+ MemoryTool = TypingAny # type: ignore[assignment,misc]
+ RAGTool = TypingAny # type: ignore[assignment,misc]
+
+
+@dataclass
+class ContextPacket:
+ """上下文信息包"""
+ content: str
+ timestamp: datetime = field(default_factory=datetime.now)
+ metadata: Dict[str, Any] = field(default_factory=dict)
+ token_count: int = 0
+ relevance_score: float = 0.0 # 0.0-1.0
+
+ def __post_init__(self):
+ """自动计算token数"""
+ if self.token_count == 0:
+ self.token_count = count_tokens(self.content)
+
+
+@dataclass
+class ContextConfig:
+ """上下文构建配置"""
+ max_tokens: int = 8000 # 总预算
+ reserve_ratio: float = 0.15 # 生成余量(10-20%)
+ min_relevance: float = 0.3 # 最小相关性阈值(仅对扩展上下文生效)
+ max_history_turns: int = 10 # 最大保留对话轮数
+ enable_mmr: bool = True # 启用最大边际相关性(多样性)
+ mmr_lambda: float = 0.7 # MMR平衡参数(0=纯多样性, 1=纯相关性)
+ system_prompt_template: str = "" # 系统提示模板
+ enable_compression: bool = True # 启用压缩
+ include_output_format: bool = True # 是否附加固定输出格式约束
+ # 按需探索模式:不主动查询 memory/rag,由模型通过工具按需获取
+ lazy_fetch: bool = True
+
+ def get_available_tokens(self) -> int:
+ """获取可用token预算(扣除余量)"""
+ return int(self.max_tokens * (1 - self.reserve_ratio))
+
+
+class ContextBuilder:
+ """上下文构建器 - GSSC流水线
+
+ 设计理念(借鉴 Claude Code):
+ - 保底上下文:系统提示 + 对话历史 + 上次工具摘要(自动注入,不可或缺)
+ - 扩展上下文:memory/rag/notes(通过工具按需获取,模型自行决定)
+
+ 用法示例:
+ ```python
+ # 方式1:只构建保底上下文(推荐,让模型按需获取扩展上下文)
+ builder = ContextBuilder(config=ContextConfig(lazy_fetch=True))
+ context = builder.build_base(
+ user_query="用户问题",
+ conversation_history=[...],
+ system_instructions="系统指令",
+ tool_summaries=[...] # 上次工具调用摘要
+ )
+
+ # 方式2:传统模式,主动收集所有上下文
+ builder = ContextBuilder(
+ memory_tool=memory_tool,
+ rag_tool=rag_tool,
+ config=ContextConfig(lazy_fetch=False)
+ )
+ context = builder.build(user_query="用户问题", ...)
+ ```
+ """
+
+ def __init__(
+ self,
+ memory_tool: Optional[MemoryTool] = None,
+ rag_tool: Optional[RAGTool] = None,
+ config: Optional[ContextConfig] = None,
+ llm: Optional[HelloAgentsLLM] = None,
+ ):
+ self.memory_tool = memory_tool
+ self.rag_tool = rag_tool
+ self.config = config or ContextConfig()
+ self.llm = llm
+ self._encoding = tiktoken.get_encoding("cl100k_base")
+
+ def build_base(
+ self,
+ user_query: str,
+ conversation_history: Optional[List[Message]] = None,
+ system_instructions: Optional[str] = None,
+ tool_summaries: Optional[List[str]] = None,
+ pending_state: Optional[str] = None,
+ ) -> str:
+ """构建保底上下文(推荐使用)
+
+ 只包含必需的基础上下文,不主动查询 memory/rag。
+ 扩展上下文由模型通过 context_fetch 工具按需获取。
+
+ 保底上下文包括:
+ - 系统指令/安全约束
+ - 最近 N 轮对话历史
+ - 上次工具调用摘要
+ - 待确认的补丁/计划状态
+
+ Args:
+ user_query: 用户查询
+ conversation_history: 对话历史
+ system_instructions: 系统指令
+ tool_summaries: 上次工具调用摘要列表
+ pending_state: 待确认的状态(补丁/计划等)
+
+ Returns:
+ 结构化上下文字符串
+ """
+ packets = []
+
+ # P0: 系统指令(必须)
+ if system_instructions:
+ packets.append(ContextPacket(
+ content=system_instructions,
+ metadata={"type": "instructions"}
+ ))
+
+ # P1: 对话历史(必须)
+ if conversation_history:
+ recent_history = conversation_history[-self.config.max_history_turns:]
+ history_text = "\n".join([
+ f"[{msg.role}] {msg.content}"
+ for msg in recent_history
+ ])
+ packets.append(ContextPacket(
+ content=history_text,
+ metadata={"type": "history", "count": len(recent_history)}
+ ))
+
+ # P2: 上次工具调用摘要(如有)
+ if tool_summaries:
+ summary_text = "\n".join(tool_summaries[-3:]) # 最多保留最近 3 条
+ packets.append(ContextPacket(
+ content=f"[上次工具结果摘要]\n{summary_text}",
+ metadata={"type": "tool_summary"}
+ ))
+
+ # P3: 待确认状态(如有)
+ if pending_state:
+ packets.append(ContextPacket(
+ content=f"[待确认状态]\n{pending_state}",
+ metadata={"type": "pending_state"}
+ ))
+
+ # 直接结构化,不做相关性过滤(保底上下文全部保留)
+ structured_context = self._structure_base(packets, user_query)
+
+ # 压缩(如果超预算)
+ final_context = self._compress(structured_context)
+ log_event(
+ "context_base",
+ {
+ "tokens": count_tokens(final_context),
+ "history_turns": len(conversation_history or []),
+ "tool_summaries": len(tool_summaries or []),
+ "lazy_fetch": self.config.lazy_fetch,
+ },
+ )
+ return final_context
+
+ def _structure_base(
+ self,
+ packets: List[ContextPacket],
+ user_query: str,
+ ) -> str:
+ """为保底上下文构建结构化模板"""
+ sections = []
+
+ # [Role & Policies]
+ instructions = [p for p in packets if p.metadata.get("type") == "instructions"]
+ if instructions:
+ sections.append("[Role & Policies]\n" + "\n".join([p.content for p in instructions]))
+
+ # [Context] - 对话历史
+ history = [p for p in packets if p.metadata.get("type") == "history"]
+ if history:
+ sections.append("[Context]\n以下是最近的对话记录:\n" + "\n".join([p.content for p in history]))
+
+ # [Evidence] - 工具摘要
+ tool_summary = [p for p in packets if p.metadata.get("type") == "tool_summary"]
+ if tool_summary:
+ sections.append("[Evidence]\n" + "\n".join([p.content for p in tool_summary]))
+
+ # [State] - 待确认状态
+ pending = [p for p in packets if p.metadata.get("type") == "pending_state"]
+ if pending:
+ sections.append("[State]\n" + "\n".join([p.content for p in pending]))
+
+ # [Task]
+ sections.append(f"[Task]\n{user_query}")
+
+ return "\n\n".join(sections)
+
+ def build(
+ self,
+ user_query: str,
+ conversation_history: Optional[List[Message]] = None,
+ system_instructions: Optional[str] = None,
+ additional_packets: Optional[List[ContextPacket]] = None
+ ) -> str:
+ """构建完整上下文
+
+ Args:
+ user_query: 用户查询
+ conversation_history: 对话历史
+ system_instructions: 系统指令
+ additional_packets: 额外的上下文包
+
+ Returns:
+ 结构化上下文字符串
+ """
+ # 1. Gather: 收集候选信息
+ packets = self._gather(
+ user_query=user_query,
+ conversation_history=conversation_history or [],
+ system_instructions=system_instructions,
+ additional_packets=additional_packets or []
+ )
+
+ # 2. Select: 筛选与排序
+ selected_packets = self._select(packets, user_query)
+
+ # 3. Structure: 组织成结构化模板
+ structured_context = self._structure(
+ selected_packets=selected_packets,
+ user_query=user_query,
+ system_instructions=system_instructions
+ )
+
+ # 4. Compress: 压缩与规范化(如果超预算)
+ final_context = self._compress(structured_context)
+
+ return final_context
+
+ def _gather(
+ self,
+ user_query: str,
+ conversation_history: List[Message],
+ system_instructions: Optional[str],
+ additional_packets: List[ContextPacket]
+ ) -> List[ContextPacket]:
+ """Gather: 收集候选信息
+
+ 当 lazy_fetch=True 时,只收集保底上下文(系统指令+对话历史+额外包)。
+ 当 lazy_fetch=False 时,主动查询 memory/rag(传统模式)。
+ """
+ packets = []
+
+ # P0: 系统指令(强约束,总是保留)
+ if system_instructions:
+ packets.append(ContextPacket(
+ content=system_instructions,
+ metadata={"type": "instructions"}
+ ))
+
+ # P1: 对话历史(基础上下文,总是保留)
+ if conversation_history:
+ recent_history = conversation_history[-self.config.max_history_turns:]
+ history_text = "\n".join([
+ f"[{msg.role}] {msg.content}"
+ for msg in recent_history
+ ])
+ packets.append(ContextPacket(
+ content=history_text,
+ metadata={"type": "history", "count": len(recent_history)}
+ ))
+
+ # 以下为扩展上下文,仅在 lazy_fetch=False 时主动收集
+ if not self.config.lazy_fetch:
+ # P2: 从记忆中获取任务状态与关键结论
+ if self.memory_tool:
+ try:
+ # 搜索任务状态相关记忆
+ state_results = self.memory_tool.execute(
+ "search",
+ query="(任务状态 OR 子目标 OR 结论 OR 阻塞)",
+ min_importance=0.7,
+ limit=5
+ )
+ if state_results and "未找到" not in state_results:
+ packets.append(ContextPacket(
+ content=state_results,
+ metadata={"type": "task_state", "importance": "high"}
+ ))
+
+ # 搜索与当前查询相关的记忆
+ related_results = self.memory_tool.execute(
+ "search",
+ query=user_query,
+ limit=5
+ )
+ if related_results and "未找到" not in related_results:
+ packets.append(ContextPacket(
+ content=related_results,
+ metadata={"type": "related_memory"}
+ ))
+ except Exception as e:
+ print(f"⚠️ 记忆检索失败: {e}")
+
+ # P3: 从RAG中获取事实证据
+ if self.rag_tool:
+ try:
+ rag_results = self.rag_tool.run({
+ "action": "search",
+ "query": user_query,
+ "top_k": 5
+ })
+ if rag_results and "未找到" not in rag_results and "错误" not in rag_results:
+ packets.append(ContextPacket(
+ content=rag_results,
+ metadata={"type": "knowledge_base"}
+ ))
+ except Exception as e:
+ print(f"⚠️ RAG检索失败: {e}")
+
+ # 添加额外包(如上次工具结果摘要)
+ packets.extend(additional_packets)
+
+ return packets
+
+ def _select(
+ self,
+ packets: List[ContextPacket],
+ user_query: str
+ ) -> List[ContextPacket]:
+ """Select: 基于分数与预算的筛选"""
+ # 1) 计算相关性(关键词重叠)
+ query_tokens = set(user_query.lower().split())
+ for packet in packets:
+ content_tokens = set(packet.content.lower().split())
+ if len(query_tokens) > 0:
+ overlap = len(query_tokens & content_tokens)
+ packet.relevance_score = overlap / len(query_tokens)
+ else:
+ packet.relevance_score = 0.0
+
+ # 2) 计算新近性(指数衰减)
+ def recency_score(ts: datetime) -> float:
+ delta = max((datetime.now() - ts).total_seconds(), 0)
+ tau = 3600 # 1小时时间尺度,可暴露到配置
+ return math.exp(-delta / tau)
+
+ # 3) 计算复合分:0.7*相关性 + 0.3*新近性
+ scored_packets: List[Tuple[float, ContextPacket]] = []
+ for p in packets:
+ rec = recency_score(p.timestamp)
+ score = 0.7 * p.relevance_score + 0.3 * rec
+ scored_packets.append((score, p))
+
+ # 4) 系统指令和对话历史单独拿出,固定纳入(这两者是基础上下文,不应被过滤)
+ must_keep_types = {"instructions", "history"}
+ must_keep_packets = [p for (_, p) in scored_packets if p.metadata.get("type") in must_keep_types]
+ remaining = [p for (s, p) in sorted(scored_packets, key=lambda x: x[0], reverse=True)
+ if p.metadata.get("type") not in must_keep_types]
+
+ # 5) 依据 min_relevance 过滤(仅对扩展上下文:memory、RAG、notes 等)
+ filtered = [p for p in remaining if p.relevance_score >= self.config.min_relevance]
+
+ # 6) 按预算填充
+ available_tokens = self.config.get_available_tokens()
+ selected: List[ContextPacket] = []
+ used_tokens = 0
+
+ # 先放入必须保留的上下文(系统指令 + 对话历史)
+ for p in must_keep_packets:
+ if used_tokens + p.token_count <= available_tokens:
+ selected.append(p)
+ used_tokens += p.token_count
+
+ # 再按分数加入其余
+ for p in filtered:
+ if used_tokens + p.token_count > available_tokens:
+ continue
+ selected.append(p)
+ used_tokens += p.token_count
+
+ return selected
+
+ def _structure(
+ self,
+ selected_packets: List[ContextPacket],
+ user_query: str,
+ system_instructions: Optional[str]
+ ) -> str:
+ """Structure: 组织成结构化上下文模板"""
+ sections = []
+
+ # [Role & Policies] - 系统指令
+ p0_packets = [p for p in selected_packets if p.metadata.get("type") == "instructions"]
+ if p0_packets:
+ role_section = "[Role & Policies]\n"
+ role_section += "\n".join([p.content for p in p0_packets])
+ sections.append(role_section)
+
+ # [Task] - 当前任务
+ sections.append(f"[Task]\n用户问题:{user_query}")
+
+ # [State] - 任务状态
+ p1_packets = [p for p in selected_packets if p.metadata.get("type") == "task_state"]
+ if p1_packets:
+ state_section = "[State]\n关键进展与未决问题:\n"
+ state_section += "\n".join([p.content for p in p1_packets])
+ sections.append(state_section)
+
+ # [Evidence] - 事实证据
+ p2_packets = [
+ p for p in selected_packets
+ if p.metadata.get("type") in {"related_memory", "knowledge_base", "retrieval", "tool_result"}
+ ]
+ if p2_packets:
+ evidence_section = "[Evidence]\n事实与引用:\n"
+ for p in p2_packets:
+ evidence_section += f"\n{p.content}\n"
+ sections.append(evidence_section)
+
+ # [Recent Conversation] - 对话历史(明确标识)
+ p3_packets = [p for p in selected_packets if p.metadata.get("type") == "history"]
+ if p3_packets:
+ context_section = "[Recent Conversation]\n以下是最近的对话记录。当用户询问之前的对话内容时,请参考此部分:\n"
+ context_section += "\n".join([p.content for p in p3_packets])
+ sections.append(context_section)
+
+ # [Output] - 输出约束(可选)
+ if self.config.include_output_format:
+ output_section = """[Output]
+请按以下格式回答:
+1. 结论(简洁明确)
+2. 依据(列出支撑证据及来源)
+3. 风险与假设(如有)
+4. 下一步行动建议(如适用)"""
+ sections.append(output_section)
+
+ return "\n\n".join(sections)
+
+ def _compress(self, context: str) -> str:
+ """Compress: 压缩与规范化"""
+ if not self.config.enable_compression:
+ return context
+
+ current_tokens = count_tokens(context)
+ available_tokens = self.config.get_available_tokens()
+
+ if current_tokens <= available_tokens:
+ return context
+
+ # LLM 压缩(更保真):仅在提供 llm 时启用
+ if self.llm is not None:
+ try:
+ target = available_tokens
+ # 尽量保留结构:Role/Task 原样,压缩 Evidence/Context
+ sys = (
+ "你是一个上下文压缩器。将输入的多段上下文压缩到目标 token 预算内,"
+ "尽量保留:用户目标、关键约束、关键证据(文件路径/命令/错误/结论)。"
+ "不要编造。保持原有分段标题([Role & Policies]/[Task]/[State]/[Evidence]/[Context] 等),"
+ "但可以大幅精简 [Evidence]/[Context] 内容。输出应尽量短。"
+ )
+ user = f"目标预算(约): {target} tokens\n\n原始上下文:\n{context}"
+ compressed = self.llm.invoke(
+ [
+ {"role": "system", "content": sys},
+ {"role": "user", "content": user},
+ ],
+ max_tokens=min(1200, int(self.config.max_tokens * 0.4)),
+ )
+ if compressed and isinstance(compressed, str) and count_tokens(compressed) <= target:
+ return compressed
+ except Exception:
+ pass
+
+ # 简单截断策略(保留前N个token)
+ # 实际应用中可用LLM做高保真摘要
+ print(f"⚠️ 上下文超预算 ({current_tokens} > {available_tokens}),执行截断")
+
+ # 按段落截断,保留结构
+ lines = context.split("\n")
+ compressed_lines = []
+ used_tokens = 0
+
+ for line in lines:
+ line_tokens = count_tokens(line)
+ if used_tokens + line_tokens > available_tokens:
+ break
+ compressed_lines.append(line)
+ used_tokens += line_tokens
+
+ return "\n".join(compressed_lines)
+
+
+def count_tokens(text: str) -> int:
+ """计算文本token数(使用tiktoken)"""
+ try:
+ encoding = tiktoken.get_encoding("cl100k_base")
+ return len(encoding.encode(text))
+ except Exception:
+ # 降级方案:粗略估算(1 token ≈ 4 字符)
+ return len(text) // 4
+
diff --git a/Co-creation-projects/aug618-Praxis/core/__init__.py b/Co-creation-projects/aug618-Praxis/core/__init__.py
new file mode 100644
index 00000000..f2500aea
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/core/__init__.py
@@ -0,0 +1,19 @@
+"""核心框架模块
+
+说明:本仓库的核心模块包含可选依赖(如 pydantic)。为便于在最小环境下
+复用部分能力(例如仅使用 LLM 客户端),这里对可选依赖做惰性/容错导入。
+"""
+
+from .exceptions import HelloAgentsException
+from .llm import HelloAgentsLLM
+
+try:
+ from .agent import Agent
+ from .config import Config
+ from .message import Message
+except Exception: # optional deps may be missing in minimal environments
+ Agent = None # type: ignore[assignment]
+ Config = None # type: ignore[assignment]
+ Message = None # type: ignore[assignment]
+
+__all__ = ["HelloAgentsLLM", "HelloAgentsException", "Agent", "Config", "Message"]
diff --git a/Co-creation-projects/aug618-Praxis/core/agent.py b/Co-creation-projects/aug618-Praxis/core/agent.py
new file mode 100644
index 00000000..157d6060
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/core/agent.py
@@ -0,0 +1,46 @@
+"""Agent基类"""
+
+from abc import ABC, abstractmethod
+from typing import Optional
+from .message import Message
+from .llm import HelloAgentsLLM
+from .config import Config
+
+class Agent(ABC):
+ """Agent基类"""
+
+ def __init__(
+ self,
+ name: str,
+ llm: HelloAgentsLLM,
+ system_prompt: Optional[str] = None,
+ config: Optional[Config] = None
+ ):
+ self.name = name
+ self.llm = llm
+ self.system_prompt = system_prompt
+ self.config = config or Config()
+ self._history: list[Message] = []
+
+ @abstractmethod
+ def run(self, input_text: str, **kwargs) -> str:
+ """运行Agent"""
+ pass
+
+ def add_message(self, message: Message):
+ """添加消息到历史记录"""
+ self._history.append(message)
+
+ def clear_history(self):
+ """清空历史记录"""
+ self._history.clear()
+
+ def get_history(self) -> list[Message]:
+ """获取历史记录"""
+ return self._history.copy()
+
+ def __str__(self) -> str:
+ return f"Agent(name={self.name}, provider={self.llm.provider})"
+
+ def __repr__(self) -> str:
+ return self.__str__()
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/core/config.py b/Co-creation-projects/aug618-Praxis/core/config.py
new file mode 100644
index 00000000..740906d0
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/core/config.py
@@ -0,0 +1,183 @@
+"""配置管理 - Code Agent CLI 统一配置"""
+
+import os
+from typing import Optional, Dict, Any, List
+from pathlib import Path
+from pydantic import BaseModel, Field
+
+
+# ==================== 模型定义 ====================
+# 预定义的模型配置
+# - multimodal: 是否支持多模态
+# - base_url: API 地址
+# - api_key_env: 对应的 API Key 环境变量名(切换模型时自动读取)
+# - description: 模型描述
+AVAILABLE_MODELS: Dict[str, Dict[str, Any]] = {
+ "glm-4.7": {
+ "multimodal": False,
+ "base_url": "https://open.bigmodel.cn/api/paas/v4",
+ "api_key_env": ["ZHIPU_API_KEY", "GLM_API_KEY"],
+ "description": "智谱 GLM-4.7 文本模型(默认)",
+ },
+ "glm-4.6v-flash": {
+ "multimodal": True,
+ "base_url": "https://open.bigmodel.cn/api/paas/v4",
+ "api_key_env": ["ZHIPU_API_KEY", "GLM_API_KEY"],
+ "description": "智谱 GLM-4.6V-Flash 多模态模型(支持图片理解)",
+ },
+ "deepseek-chat": {
+ "multimodal": False,
+ "base_url": "https://api.deepseek.com",
+ "api_key_env": ["DEEPSEEK_API_KEY"],
+ "description": "DeepSeek Chat 文本模型",
+ },
+ "qwen-plus": {
+ "multimodal": False,
+ "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ "api_key_env": ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
+ "description": "通义千问 Plus 文本模型",
+ },
+ "qwen-vl-plus": {
+ "multimodal": True,
+ "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
+ "api_key_env": ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
+ "description": "通义千问 VL Plus 多模态模型",
+ },
+}
+
+def is_multimodal_model(model_name: str) -> bool:
+ """判断模型是否支持多模态"""
+ if model_name in AVAILABLE_MODELS:
+ return AVAILABLE_MODELS[model_name].get("multimodal", False)
+ # 启发式判断:模型名包含 v, vl, vision 等关键词
+ lower = model_name.lower()
+ return any(kw in lower for kw in ["vision", "-vl", "-v-", "4v", "4.6v", "4o"])
+
+
+class Config(BaseModel):
+ """Code Agent CLI 统一配置类
+
+ 集中管理所有配置项,支持:
+ - 环境变量加载
+ - 默认值设置
+ - 类型验证
+ """
+
+ # ==================== 基础配置 ====================
+ project_name: str = Field(default="code_agent", description="项目名称")
+ debug: bool = Field(default=False, description="调试模式")
+ log_level: str = Field(default="INFO", description="日志级别")
+
+ # ==================== LLM 配置 ====================
+ default_model: str = Field(default="glm-4.7", description="默认模型")
+ default_provider: str = Field(default="zhipu", description="默认提供商")
+ temperature: float = Field(default=0.7, ge=0.0, le=2.0, description="温度参数")
+ max_tokens: Optional[int] = Field(default=None, description="最大 token 数")
+ llm_timeout: int = Field(default=60, gt=0, description="LLM 请求超时(秒)")
+
+
+ # ==================== Agent 配置 ====================
+ max_react_steps: int = Field(default=20, gt=0, le=50, description="ReAct 最大步数")
+ max_history_turns: int = Field(default=50, gt=0, description="最大历史对话轮数")
+ observation_summary_threshold: int = Field(default=2000, gt=0, description="工具输出摘要阈值")
+
+ # ==================== 上下文配置 ====================
+ context_max_tokens: int = Field(default=8000, gt=0, description="上下文最大 token 数")
+ context_reserve_ratio: float = Field(default=0.15, ge=0.0, le=0.5, description="生成预留比例")
+ context_enable_compression: bool = Field(default=True, description="启用上下文压缩")
+ context_lazy_fetch: bool = Field(default=True, description="按需获取上下文")
+
+ # ==================== 工具配置 ====================
+ terminal_timeout: int = Field(default=60, gt=0, description="终端命令超时(秒)")
+ terminal_max_output_size: int = Field(default=10 * 1024 * 1024, gt=0, description="终端输出最大大小")
+ terminal_confirm_dangerous: bool = Field(default=True, description="危险命令需要确认")
+ terminal_allow_shell_mode: bool = Field(default=True, description="允许 Shell 模式")
+ context_fetch_max_tokens: int = Field(default=800, gt=0, description="单个数据源最大 token")
+ context_fetch_context_lines: int = Field(default=5, ge=0, description="代码上下文行数")
+
+ # ==================== 补丁执行器配置 ====================
+ patch_max_files: int = Field(default=10, gt=0, description="单个补丁最大文件数")
+ patch_max_total_lines: int = Field(default=800, gt=0, description="单个补丁最大总行数")
+ patch_allowed_suffixes: List[str] = Field(
+ default=[".py", ".md", ".toml", ".json", ".yml", ".yaml", ".txt", ".html", ".css", ".js", ".ts"],
+ description="允许修改的文件后缀"
+ )
+
+ # ==================== 存储配置 ====================
+ helloagents_dir: str = Field(default=".helloagents", description="状态存储目录")
+
+ # ==================== 安全配置 ====================
+ confirm_delete_files: bool = Field(default=True, description="删除文件需要确认")
+ confirm_large_changes: bool = Field(default=True, description="大规模变更需要确认")
+ large_change_threshold_files: int = Field(default=6, gt=0, description="大规模变更文件数阈值")
+ large_change_threshold_lines: int = Field(default=400, gt=0, description="大规模变更行数阈值")
+
+ @classmethod
+ def from_env(cls, **overrides) -> "Config":
+ """从环境变量创建配置
+
+ 环境变量命名规则:
+ - CODE_AGENT_<配置项大写> 或传统命名
+
+ Args:
+ **overrides: 手动覆盖的配置项
+ """
+ env_config = {
+ "debug": os.getenv("DEBUG", "false").lower() == "true" or os.getenv("CODE_AGENT_DEBUG", "false").lower() == "true",
+ "log_level": os.getenv("LOG_LEVEL", "INFO"),
+ "temperature": float(os.getenv("TEMPERATURE", "0.7")),
+ "helloagents_dir": os.getenv("HELLOAGENTS_DIR", os.getenv("CODE_AGENT_STATE_DIR", ".helloagents")),
+ "max_react_steps": int(os.getenv("CODE_AGENT_MAX_REACT_STEPS", os.getenv("CODE_AGENT_MAX_STEPS", "20"))),
+ "llm_timeout": int(os.getenv("LLM_TIMEOUT", "60")),
+ "terminal_timeout": int(os.getenv("CODE_AGENT_TERMINAL_TIMEOUT", "60")),
+ "patch_max_files": int(os.getenv("CODE_AGENT_PATCH_MAX_FILES", "10")),
+ "patch_max_total_lines": int(os.getenv("CODE_AGENT_PATCH_MAX_LINES", "800")),
+ }
+
+ if os.getenv("MAX_TOKENS"):
+ env_config["max_tokens"] = int(os.getenv("MAX_TOKENS"))
+
+ # 合并覆盖配置
+ env_config.update(overrides)
+
+ return cls(**env_config)
+
+ def get_state_dir(self, repo_root: Path) -> Path:
+ """获取状态存储目录的绝对路径"""
+ state_path = Path(self.helloagents_dir)
+ if state_path.is_absolute():
+ return state_path
+ return repo_root / state_path
+
+ def get_notes_dir(self, repo_root: Path) -> Path:
+ """获取笔记目录"""
+ return self.get_state_dir(repo_root) / "notes"
+
+ def get_sessions_dir(self, repo_root: Path) -> Path:
+ """获取会话目录"""
+ return self.get_state_dir(repo_root) / "sessions"
+
+ def get_backups_dir(self, repo_root: Path) -> Path:
+ """获取备份目录"""
+ return self.get_state_dir(repo_root) / "backups"
+
+ def get_todos_dir(self, repo_root: Path) -> Path:
+ """获取待办目录"""
+ return self.get_state_dir(repo_root) / "todos"
+
+ def to_dict(self) -> Dict[str, Any]:
+ """转换为字典"""
+ return self.model_dump()
+
+ def print_summary(self):
+ """打印配置摘要"""
+ print("=" * 50)
+ print("Code Agent CLI 配置")
+ print("=" * 50)
+ print(f"调试模式: {self.debug}")
+ print(f"ReAct 步数: {self.max_react_steps}")
+ print(f"历史轮数: {self.max_history_turns}")
+ print(f"终端超时: {self.terminal_timeout}s")
+ print(f"补丁限制: {self.patch_max_files} 文件, {self.patch_max_total_lines} 行")
+ print(f"状态目录: {self.helloagents_dir}")
+ print("=" * 50)
diff --git a/Co-creation-projects/aug618-Praxis/core/database_config.py b/Co-creation-projects/aug618-Praxis/core/database_config.py
new file mode 100644
index 00000000..e37a0f57
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/core/database_config.py
@@ -0,0 +1,195 @@
+"""
+数据库配置管理
+支持Qdrant向量数据库和Neo4j图数据库的配置
+"""
+
+import os
+from dotenv import load_dotenv
+from typing import Dict, Any, Optional
+from pydantic import BaseModel, Field
+import logging
+
+logger = logging.getLogger(__name__)
+
+# Load environment variables early so DB configs pick them up
+load_dotenv()
+
+
+class QdrantConfig(BaseModel):
+ """Qdrant向量数据库配置"""
+
+ # 连接配置
+ url: Optional[str] = Field(
+ default=None,
+ description="Qdrant服务URL (云服务或自定义URL)"
+ )
+ api_key: Optional[str] = Field(
+ default=None,
+ description="Qdrant API密钥 (云服务需要)"
+ )
+
+ # 集合配置
+ collection_name: str = Field(
+ default="hello_agents_vectors",
+ description="向量集合名称"
+ )
+ vector_size: int = Field(
+ default=384,
+ description="向量维度"
+ )
+ distance: str = Field(
+ default="cosine",
+ description="距离度量方式 (cosine, dot, euclidean)"
+ )
+
+ # 连接配置
+ timeout: int = Field(
+ default=30,
+ description="连接超时时间(秒)"
+ )
+
+ @classmethod
+ def from_env(cls) -> "QdrantConfig":
+ """从环境变量创建配置"""
+ return cls(
+ url=os.getenv("QDRANT_URL"),
+ api_key=os.getenv("QDRANT_API_KEY"),
+ collection_name=os.getenv("QDRANT_COLLECTION", "hello_agents_vectors"),
+ vector_size=int(os.getenv("QDRANT_VECTOR_SIZE", "384")),
+ distance=os.getenv("QDRANT_DISTANCE", "cosine"),
+ timeout=int(os.getenv("QDRANT_TIMEOUT", "30"))
+ )
+
+ def to_dict(self) -> Dict[str, Any]:
+ """转换为字典"""
+ return self.model_dump(exclude_none=True)
+
+
+class Neo4jConfig(BaseModel):
+ """Neo4j图数据库配置"""
+
+ # 连接配置
+ uri: str = Field(
+ default="bolt://localhost:7687",
+ description="Neo4j连接URI"
+ )
+ username: str = Field(
+ default="neo4j",
+ description="用户名"
+ )
+ password: str = Field(
+ default="hello-agents-password",
+ description="密码"
+ )
+ database: str = Field(
+ default="neo4j",
+ description="数据库名称"
+ )
+
+ # 连接池配置
+ max_connection_lifetime: int = Field(
+ default=3600,
+ description="最大连接生命周期(秒)"
+ )
+ max_connection_pool_size: int = Field(
+ default=50,
+ description="最大连接池大小"
+ )
+ connection_acquisition_timeout: int = Field(
+ default=60,
+ description="连接获取超时(秒)"
+ )
+
+ @classmethod
+ def from_env(cls) -> "Neo4jConfig":
+ """从环境变量创建配置"""
+ return cls(
+ uri=os.getenv("NEO4J_URI", "bolt://localhost:7687"),
+ username=os.getenv("NEO4J_USERNAME", "neo4j"),
+ password=os.getenv("NEO4J_PASSWORD", "hello-agents-password"),
+ database=os.getenv("NEO4J_DATABASE", "neo4j"),
+ max_connection_lifetime=int(os.getenv("NEO4J_MAX_CONNECTION_LIFETIME", "3600")),
+ max_connection_pool_size=int(os.getenv("NEO4J_MAX_CONNECTION_POOL_SIZE", "50")),
+ connection_acquisition_timeout=int(os.getenv("NEO4J_CONNECTION_TIMEOUT", "60"))
+ )
+
+ def to_dict(self) -> Dict[str, Any]:
+ """转换为字典"""
+ return self.model_dump()
+
+
+class DatabaseConfig(BaseModel):
+ """数据库配置管理器"""
+
+ qdrant: QdrantConfig = Field(
+ default_factory=QdrantConfig,
+ description="Qdrant向量数据库配置"
+ )
+ neo4j: Neo4jConfig = Field(
+ default_factory=Neo4jConfig,
+ description="Neo4j图数据库配置"
+ )
+
+ @classmethod
+ def from_env(cls) -> "DatabaseConfig":
+ """从环境变量创建配置"""
+ return cls(
+ qdrant=QdrantConfig.from_env(),
+ neo4j=Neo4jConfig.from_env()
+ )
+
+ def get_qdrant_config(self) -> Dict[str, Any]:
+ """获取Qdrant配置字典"""
+ return self.qdrant.to_dict()
+
+ def get_neo4j_config(self) -> Dict[str, Any]:
+ """获取Neo4j配置字典"""
+ return self.neo4j.to_dict()
+
+ def validate_connections(self) -> Dict[str, bool]:
+ """验证数据库连接配置"""
+ results = {}
+
+ # 验证Qdrant配置
+ try:
+ from ..memory.storage.qdrant_store import QdrantVectorStore
+ qdrant_store = QdrantVectorStore(**self.get_qdrant_config())
+ results["qdrant"] = qdrant_store.health_check()
+ logger.info(f"✅ Qdrant连接验证: {'成功' if results['qdrant'] else '失败'}")
+ except Exception as e:
+ results["qdrant"] = False
+ logger.error(f"❌ Qdrant连接验证失败: {e}")
+
+ # 验证Neo4j配置
+ try:
+ from ..memory.storage.neo4j_store import Neo4jGraphStore
+ neo4j_store = Neo4jGraphStore(**self.get_neo4j_config())
+ results["neo4j"] = neo4j_store.health_check()
+ logger.info(f"✅ Neo4j连接验证: {'成功' if results['neo4j'] else '失败'}")
+ except Exception as e:
+ results["neo4j"] = False
+ logger.error(f"❌ Neo4j连接验证失败: {e}")
+
+ return results
+
+
+# 全局配置实例
+db_config = DatabaseConfig.from_env()
+
+
+def get_database_config() -> DatabaseConfig:
+ """获取数据库配置"""
+ return db_config
+
+
+def update_database_config(**kwargs) -> None:
+ """更新数据库配置"""
+ global db_config
+
+ if "qdrant" in kwargs:
+ db_config.qdrant = QdrantConfig(**kwargs["qdrant"])
+
+ if "neo4j" in kwargs:
+ db_config.neo4j = Neo4jConfig(**kwargs["neo4j"])
+
+ logger.info("✅ 数据库配置已更新")
diff --git a/Co-creation-projects/aug618-Praxis/core/exceptions.py b/Co-creation-projects/aug618-Praxis/core/exceptions.py
new file mode 100644
index 00000000..bffde627
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/core/exceptions.py
@@ -0,0 +1,21 @@
+"""异常体系"""
+
+class HelloAgentsException(Exception):
+ """HelloAgents基础异常类"""
+ pass
+
+class LLMException(HelloAgentsException):
+ """LLM相关异常"""
+ pass
+
+class AgentException(HelloAgentsException):
+ """Agent相关异常"""
+ pass
+
+class ConfigException(HelloAgentsException):
+ """配置相关异常"""
+ pass
+
+class ToolException(HelloAgentsException):
+ """工具相关异常"""
+ pass
diff --git a/Co-creation-projects/aug618-Praxis/core/llm.py b/Co-creation-projects/aug618-Praxis/core/llm.py
new file mode 100644
index 00000000..59406d9a
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/core/llm.py
@@ -0,0 +1,440 @@
+"""HelloAgents统一LLM接口 - 基于OpenAI原生API
+
+This client is intentionally compatible with OpenAI-style chat messages.
+For multimodal models, message["content"] may be a list of content parts
+(e.g. [{"type":"text","text":"..."}, {"type":"image_url", ...}]).
+"""
+
+import os
+import time
+from typing import Any, Iterator, Literal, Optional
+from openai import OpenAI
+
+from .exceptions import HelloAgentsException
+from .config import is_multimodal_model, AVAILABLE_MODELS
+from utils.observability import log_event, estimate_prompt_tokens, estimate_completion_tokens
+
+# 支持的LLM提供商
+SUPPORTED_PROVIDERS = Literal[
+ "openai", "deepseek", "qwen", "modelscope",
+ "kimi", "zhipu", "ollama", "vllm", "local", "auto"
+]
+
+class HelloAgentsLLM:
+ """
+ 为HelloAgents定制的LLM客户端。
+ 它用于调用任何兼容OpenAI接口的服务,并默认使用流式响应。
+
+ 设计理念:
+ - 参数优先,环境变量兜底
+ - 流式响应为默认,提供更好的用户体验
+ - 支持多种LLM提供商
+ - 统一的调用接口
+ """
+
+ def __init__(
+ self,
+ model: Optional[str] = None,
+ api_key: Optional[str] = None,
+ base_url: Optional[str] = None,
+ provider: Optional[SUPPORTED_PROVIDERS] = None,
+ temperature: float = 0.7,
+ max_tokens: Optional[int] = None,
+ timeout: Optional[int] = None,
+ **kwargs
+ ):
+ """
+ 初始化客户端。优先使用传入参数,如果未提供,则从环境变量加载。
+ 支持自动检测provider或使用统一的LLM_*环境变量配置。
+
+ Args:
+ model: 模型名称,如果未提供则从环境变量LLM_MODEL_ID读取
+ api_key: API密钥,如果未提供则从环境变量读取
+ base_url: 服务地址,如果未提供则从环境变量LLM_BASE_URL读取
+ provider: LLM提供商,如果未提供则自动检测
+ temperature: 温度参数
+ max_tokens: 最大token数
+ timeout: 超时时间,从环境变量LLM_TIMEOUT读取,默认60秒
+ """
+ # 优先使用传入参数,如果未提供,则从环境变量加载
+ self.model = model or os.getenv("LLM_MODEL_ID")
+ self.temperature = temperature
+ self.max_tokens = max_tokens
+ self.timeout = timeout or int(os.getenv("LLM_TIMEOUT", "60"))
+ self.kwargs = kwargs
+
+ # 自动检测provider或使用指定的provider
+ self.provider = provider or self._auto_detect_provider(api_key, base_url)
+
+ # 根据provider确定API密钥和base_url
+ self.api_key, self.base_url = self._resolve_credentials(api_key, base_url)
+
+ # 验证必要参数
+ if not self.model:
+ self.model = self._get_default_model()
+ if not all([self.api_key, self.base_url]):
+ raise HelloAgentsException("API密钥和服务地址必须被提供或在.env文件中定义。")
+
+ # 创建OpenAI客户端
+ self._client = self._create_client()
+
+ @property
+ def is_multimodal(self) -> bool:
+ """判断当前模型是否支持多模态"""
+ return is_multimodal_model(self.model)
+
+ def switch_model(self, model_name: str, base_url: Optional[str] = None, api_key: Optional[str] = None) -> None:
+ """
+ 切换到指定模型
+
+ Args:
+ model_name: 模型名称(如 glm-4.7, glm-4.6v-flash)
+ base_url: 可选的 base_url,如果不提供则尝试从预定义配置获取
+ api_key: 可选的 api_key,如果不提供则尝试从预定义配置的环境变量获取
+ """
+ self.model = model_name
+ need_rebuild = False
+
+ if model_name in AVAILABLE_MODELS:
+ info = AVAILABLE_MODELS[model_name]
+
+ # 更新 base_url
+ new_base_url = base_url or info.get("base_url")
+ if new_base_url and new_base_url != self.base_url:
+ self.base_url = new_base_url
+ need_rebuild = True
+
+ # 更新 api_key(从对应环境变量读取)
+ if api_key:
+ self.api_key = api_key
+ need_rebuild = True
+ elif info.get("api_key_env"):
+ # 支持多个环境变量名,按优先级尝试
+ env_names = info["api_key_env"]
+ if isinstance(env_names, str):
+ env_names = [env_names]
+ for env_name in env_names:
+ new_key = os.getenv(env_name)
+ if new_key:
+ if new_key != self.api_key:
+ self.api_key = new_key
+ need_rebuild = True
+ break
+ else:
+ # 未知模型,使用传入参数
+ if base_url and base_url != self.base_url:
+ self.base_url = base_url
+ need_rebuild = True
+ if api_key and api_key != self.api_key:
+ self.api_key = api_key
+ need_rebuild = True
+
+ # 重建客户端
+ if need_rebuild:
+ self._client = self._create_client()
+
+ def _auto_detect_provider(self, api_key: Optional[str], base_url: Optional[str]) -> str:
+ """
+ 自动检测LLM提供商
+
+ 检测逻辑:
+ 1. 优先检查特定提供商的环境变量
+ 2. 根据API密钥格式判断
+ 3. 根据base_url判断
+ 4. 默认返回通用配置
+ """
+ # 1. 检查特定提供商的环境变量
+ if os.getenv("ZHIPU_API_KEY") or os.getenv("GLM_API_KEY"):
+ return "zhipu"
+ if os.getenv("DEEPSEEK_API_KEY"):
+ return "deepseek"
+ if os.getenv("DASHSCOPE_API_KEY"):
+ return "qwen"
+ if os.getenv("MODELSCOPE_API_KEY"):
+ return "modelscope"
+ if os.getenv("KIMI_API_KEY") or os.getenv("MOONSHOT_API_KEY"):
+ return "kimi"
+ if os.getenv("OPENAI_API_KEY"):
+ return "openai"
+ if os.getenv("OLLAMA_API_KEY") or os.getenv("OLLAMA_HOST"):
+ return "ollama"
+ if os.getenv("VLLM_API_KEY") or os.getenv("VLLM_HOST"):
+ return "vllm"
+
+ # 2. 根据API密钥格式判断
+ actual_api_key = api_key or os.getenv("LLM_API_KEY")
+ if actual_api_key:
+ actual_key_lower = actual_api_key.lower()
+ if actual_api_key.startswith("ms-"):
+ return "modelscope"
+ elif actual_key_lower == "ollama":
+ return "ollama"
+ elif actual_key_lower == "vllm":
+ return "vllm"
+ elif actual_key_lower == "local":
+ return "local"
+ elif actual_api_key.startswith("sk-") and len(actual_api_key) > 50:
+ # 可能是OpenAI、DeepSeek或Kimi,需要进一步判断
+ pass
+ elif actual_api_key.endswith(".") or "." in actual_api_key[-20:]:
+ # 智谱AI的API密钥格式通常包含点号
+ return "zhipu"
+
+ # 3. 根据base_url判断
+ actual_base_url = base_url or os.getenv("LLM_BASE_URL")
+ if actual_base_url:
+ base_url_lower = actual_base_url.lower()
+ if "api.openai.com" in base_url_lower:
+ return "openai"
+ elif "api.deepseek.com" in base_url_lower:
+ return "deepseek"
+ elif "dashscope.aliyuncs.com" in base_url_lower:
+ return "qwen"
+ elif "api-inference.modelscope.cn" in base_url_lower:
+ return "modelscope"
+ elif "api.moonshot.cn" in base_url_lower:
+ return "kimi"
+ elif "open.bigmodel.cn" in base_url_lower:
+ return "zhipu"
+ elif "localhost" in base_url_lower or "127.0.0.1" in base_url_lower:
+ # 本地部署检测 - 优先检查特定服务
+ if ":11434" in base_url_lower or "ollama" in base_url_lower:
+ return "ollama"
+ elif ":8000" in base_url_lower and "vllm" in base_url_lower:
+ return "vllm"
+ elif ":8080" in base_url_lower or ":7860" in base_url_lower:
+ return "local"
+ else:
+ # 根据API密钥进一步判断
+ if actual_api_key and actual_api_key.lower() == "ollama":
+ return "ollama"
+ elif actual_api_key and actual_api_key.lower() == "vllm":
+ return "vllm"
+ else:
+ return "local"
+ elif any(port in base_url_lower for port in [":8080", ":7860", ":5000"]):
+ # 常见的本地部署端口
+ return "local"
+
+ # 4. 默认返回auto,使用通用配置
+ return "auto"
+
+ def _resolve_credentials(self, api_key: Optional[str], base_url: Optional[str]) -> tuple[str, str]:
+ """根据provider解析API密钥和base_url"""
+ if self.provider == "openai":
+ resolved_api_key = api_key or os.getenv("OPENAI_API_KEY") or os.getenv("LLM_API_KEY")
+ resolved_base_url = base_url or os.getenv("LLM_BASE_URL") or "https://api.openai.com/v1"
+ return resolved_api_key, resolved_base_url
+
+ elif self.provider == "deepseek":
+ resolved_api_key = api_key or os.getenv("DEEPSEEK_API_KEY") or os.getenv("LLM_API_KEY")
+ resolved_base_url = base_url or os.getenv("LLM_BASE_URL") or "https://api.deepseek.com"
+ return resolved_api_key, resolved_base_url
+
+ elif self.provider == "qwen":
+ resolved_api_key = api_key or os.getenv("DASHSCOPE_API_KEY") or os.getenv("LLM_API_KEY")
+ resolved_base_url = base_url or os.getenv("DASHSCOPE_BASE_URL") or "https://dashscope.aliyuncs.com/compatible-mode/v1"
+ return resolved_api_key, resolved_base_url
+
+ elif self.provider == "modelscope":
+ resolved_api_key = api_key or os.getenv("MODELSCOPE_API_KEY") or os.getenv("LLM_API_KEY")
+ resolved_base_url = base_url or os.getenv("LLM_BASE_URL") or "https://api-inference.modelscope.cn/v1/"
+ return resolved_api_key, resolved_base_url
+
+ elif self.provider == "kimi":
+ resolved_api_key = api_key or os.getenv("KIMI_API_KEY") or os.getenv("MOONSHOT_API_KEY") or os.getenv("LLM_API_KEY")
+ resolved_base_url = base_url or os.getenv("LLM_BASE_URL") or "https://api.moonshot.cn/v1"
+ return resolved_api_key, resolved_base_url
+
+ elif self.provider == "zhipu":
+ resolved_api_key = api_key or os.getenv("ZHIPU_API_KEY") or os.getenv("GLM_API_KEY") or os.getenv("LLM_API_KEY")
+ resolved_base_url = base_url or os.getenv("LLM_BASE_URL") or "https://open.bigmodel.cn/api/paas/v4"
+ return resolved_api_key, resolved_base_url
+
+ elif self.provider == "ollama":
+ resolved_api_key = api_key or os.getenv("OLLAMA_API_KEY") or os.getenv("LLM_API_KEY") or "ollama"
+ resolved_base_url = base_url or os.getenv("OLLAMA_HOST") or os.getenv("LLM_BASE_URL") or "http://localhost:11434/v1"
+ return resolved_api_key, resolved_base_url
+
+ elif self.provider == "vllm":
+ resolved_api_key = api_key or os.getenv("VLLM_API_KEY") or os.getenv("LLM_API_KEY") or "vllm"
+ resolved_base_url = base_url or os.getenv("VLLM_HOST") or os.getenv("LLM_BASE_URL") or "http://localhost:8000/v1"
+ return resolved_api_key, resolved_base_url
+
+ elif self.provider == "local":
+ resolved_api_key = api_key or os.getenv("LLM_API_KEY") or "local"
+ resolved_base_url = base_url or os.getenv("LLM_BASE_URL") or "http://localhost:8000/v1"
+ return resolved_api_key, resolved_base_url
+
+ else:
+ # auto或其他情况:使用通用配置,支持任何OpenAI兼容的服务
+ resolved_api_key = api_key or os.getenv("LLM_API_KEY")
+ resolved_base_url = base_url or os.getenv("LLM_BASE_URL")
+ return resolved_api_key, resolved_base_url
+
+ def _create_client(self) -> OpenAI:
+ """创建OpenAI客户端"""
+ return OpenAI(
+ api_key=self.api_key,
+ base_url=self.base_url,
+ timeout=self.timeout
+ )
+
+ def _get_default_model(self) -> str:
+ """获取默认模型"""
+ if self.provider == "openai":
+ return "gpt-3.5-turbo"
+ elif self.provider == "deepseek":
+ return "deepseek-chat"
+ elif self.provider == "qwen":
+ return "qwen-plus"
+ elif self.provider == "modelscope":
+ return "Qwen/Qwen2.5-72B-Instruct"
+ elif self.provider == "kimi":
+ return "moonshot-v1-8k"
+ elif self.provider == "zhipu":
+ return "glm-4"
+ elif self.provider == "ollama":
+ return "llama3.2" # Ollama常用模型
+ elif self.provider == "vllm":
+ return "meta-llama/Llama-2-7b-chat-hf" # vLLM常用模型
+ elif self.provider == "local":
+ return "local-model" # 本地模型占位符
+ else:
+ # auto或其他情况:根据base_url智能推断默认模型
+ base_url = os.getenv("LLM_BASE_URL", "")
+ base_url_lower = base_url.lower()
+ if "modelscope" in base_url_lower:
+ return "Qwen/Qwen2.5-72B-Instruct"
+ elif "deepseek" in base_url_lower:
+ return "deepseek-chat"
+ elif "dashscope" in base_url_lower:
+ return "qwen-plus"
+ elif "moonshot" in base_url_lower:
+ return "moonshot-v1-8k"
+ elif "bigmodel" in base_url_lower:
+ return "glm-4"
+ elif "ollama" in base_url_lower or ":11434" in base_url_lower:
+ return "llama3.2"
+ elif ":8000" in base_url_lower or "vllm" in base_url_lower:
+ return "meta-llama/Llama-2-7b-chat-hf"
+ elif "localhost" in base_url_lower or "127.0.0.1" in base_url_lower:
+ return "local-model"
+ else:
+ return "gpt-3.5-turbo"
+
+ def think(self, messages: list[dict[str, Any]], temperature: Optional[float] = None) -> Iterator[str]:
+ """
+ 调用大语言模型进行思考,并返回流式响应。
+ 这是主要的调用方法,默认使用流式响应以获得更好的用户体验。
+
+ Args:
+ messages: 消息列表
+ temperature: 温度参数,如果未提供则使用初始化时的值
+
+ Yields:
+ str: 流式响应的文本片段
+ """
+ print(f"🧠 正在调用 {self.model} 模型...")
+ start = time.time()
+ stream_text_parts: list[str] = []
+ try:
+ response = self._client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=temperature if temperature is not None else self.temperature,
+ max_tokens=self.max_tokens,
+ stream=True,
+ )
+
+ # 处理流式响应
+ print("✅ 大语言模型响应成功:")
+ for chunk in response:
+ content = chunk.choices[0].delta.content or ""
+ if content:
+ print(content, end="", flush=True)
+ stream_text_parts.append(content)
+ yield content
+ print() # 在流式输出结束后换行
+ completion_text = "".join(stream_text_parts)
+ log_event(
+ "llm",
+ {
+ "model": self.model,
+ "provider": self.provider,
+ "stream": True,
+ "ok": True,
+ "ms": int((time.time() - start) * 1000),
+ "prompt_tokens_est": estimate_prompt_tokens(messages),
+ "completion_tokens_est": estimate_completion_tokens(completion_text),
+ },
+ )
+
+ except Exception as e:
+ print(f"❌ 调用LLM API时发生错误: {e}")
+ log_event(
+ "llm",
+ {
+ "model": self.model,
+ "provider": self.provider,
+ "stream": True,
+ "ok": False,
+ "ms": int((time.time() - start) * 1000),
+ "error": str(e),
+ },
+ )
+ raise HelloAgentsException(f"LLM调用失败: {str(e)}")
+
+ def invoke(self, messages: list[dict[str, Any]], **kwargs) -> str:
+ """
+ 非流式调用LLM,返回完整响应。
+ 适用于不需要流式输出的场景。
+ """
+ start = time.time()
+ try:
+ response = self._client.chat.completions.create(
+ model=self.model,
+ messages=messages,
+ temperature=kwargs.get('temperature', self.temperature),
+ max_tokens=kwargs.get('max_tokens', self.max_tokens),
+ **{k: v for k, v in kwargs.items() if k not in ['temperature', 'max_tokens']}
+ )
+ content = response.choices[0].message.content
+ usage = getattr(response, "usage", None)
+ log_event(
+ "llm",
+ {
+ "model": self.model,
+ "provider": self.provider,
+ "stream": False,
+ "ok": True,
+ "ms": int((time.time() - start) * 1000),
+ "prompt_tokens": getattr(usage, "prompt_tokens", None),
+ "completion_tokens": getattr(usage, "completion_tokens", None),
+ "total_tokens": getattr(usage, "total_tokens", None),
+ "prompt_tokens_est": estimate_prompt_tokens(messages),
+ "completion_tokens_est": estimate_completion_tokens(content),
+ },
+ )
+ return content
+ except Exception as e:
+ log_event(
+ "llm",
+ {
+ "model": self.model,
+ "provider": self.provider,
+ "stream": False,
+ "ok": False,
+ "ms": int((time.time() - start) * 1000),
+ "error": str(e),
+ },
+ )
+ raise HelloAgentsException(f"LLM调用失败: {str(e)}")
+
+ def stream_invoke(self, messages: list[dict[str, Any]], **kwargs) -> Iterator[str]:
+ """
+ 流式调用LLM的别名方法,与think方法功能相同。
+ 保持向后兼容性。
+ """
+ temperature = kwargs.get('temperature')
+ yield from self.think(messages, temperature)
diff --git a/Co-creation-projects/aug618-Praxis/core/message.py b/Co-creation-projects/aug618-Praxis/core/message.py
new file mode 100644
index 00000000..a69fe630
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/core/message.py
@@ -0,0 +1,33 @@
+"""消息系统"""
+
+from typing import Optional, Dict, Any, Literal
+from datetime import datetime
+from pydantic import BaseModel
+
+MessageRole = Literal["user", "assistant", "system", "tool"]
+
+class Message(BaseModel):
+ """消息类"""
+
+ content: str
+ role: MessageRole
+ timestamp: datetime = None
+ metadata: Optional[Dict[str, Any]] = None
+
+ def __init__(self, content: str, role: MessageRole, **kwargs):
+ super().__init__(
+ content=content,
+ role=role,
+ timestamp=kwargs.get('timestamp', datetime.now()),
+ metadata=kwargs.get('metadata', {})
+ )
+
+ def to_dict(self) -> Dict[str, Any]:
+ """转换为字典格式(OpenAI API格式)"""
+ return {
+ "role": self.role,
+ "content": self.content
+ }
+
+ def __str__(self) -> str:
+ return f"[{self.role}] {self.content}"
diff --git a/Co-creation-projects/aug618-Praxis/env.example b/Co-creation-projects/aug618-Praxis/env.example
new file mode 100644
index 00000000..7e36f5d0
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/env.example
@@ -0,0 +1,225 @@
+
+# -------------------------------------------------
+# 基础运行开关
+# -------------------------------------------------
+
+# 调试模式(等价:CODE_AGENT_DEBUG)
+DEBUG=false
+CODE_AGENT_DEBUG=false
+
+# 日志级别:DEBUG / INFO / WARNING / ERROR
+LOG_LEVEL=INFO
+
+# 关闭彩色输出(CLI/TUI 里可能用到)
+NO_COLOR=
+
+# 采样温度(0.0-2.0)
+TEMPERATURE=0.7
+
+# 生成最大 token(可选;不设置则按模型/实现默认)
+MAX_TOKENS=
+
+
+# -------------------------------------------------
+# Code Agent 核心配置(core/config.py)
+# -------------------------------------------------
+
+# 状态目录(等价:CODE_AGENT_STATE_DIR)
+# 默认:.helloagents(相对 repo 根目录)
+HELLOAGENTS_DIR=.helloagents
+CODE_AGENT_STATE_DIR=.helloagents
+
+# ReAct 最大步数(等价:CODE_AGENT_MAX_STEPS)
+CODE_AGENT_MAX_REACT_STEPS=20
+CODE_AGENT_MAX_STEPS=20
+
+# 终端工具超时(秒)
+CODE_AGENT_TERMINAL_TIMEOUT=60
+
+# Patch 限制
+CODE_AGENT_PATCH_MAX_FILES=10
+CODE_AGENT_PATCH_MAX_LINES=800
+
+
+# -------------------------------------------------
+# 观测/日志(utils/observability.py)
+# -------------------------------------------------
+
+# 日志目录(默认会自动设置到 /logs)
+CODE_AGENT_LOG_DIR=
+
+# 会话 ID(运行时自动写入;一般不需要手动设置)
+CODE_AGENT_SESSION_ID=
+
+
+# -------------------------------------------------
+# LLM 通用配置(core/llm.py)
+# -------------------------------------------------
+
+# 模型 ID(例如:glm-4.7 / deepseek-chat / qwen-plus / 本地模型名等)
+LLM_MODEL_ID=
+
+# 通用 API Key(如果下方厂商专用 key 未设置,会回退到这个)
+LLM_API_KEY=
+
+# 通用 Base URL(如果下方厂商专用 base_url/host 未设置,会回退到这个)
+LLM_BASE_URL=
+
+# LLM 请求超时(秒)
+LLM_TIMEOUT=60
+
+
+# -------------------------------------------------
+# LLM 厂商/后端专用配置(core/llm.py)
+# (你只需要配置你在用的那一个/几个)
+# -------------------------------------------------
+
+# OpenAI 兼容
+OPENAI_API_KEY=
+
+# DeepSeek
+DEEPSEEK_API_KEY=
+
+# 阿里 DashScope / Qwen(OpenAI compatible-mode)
+DASHSCOPE_API_KEY=
+# 可选:单独的 DashScope base_url(否则走 LLM_BASE_URL 或默认)
+DASHSCOPE_BASE_URL=
+
+# ModelScope
+MODELSCOPE_API_KEY=
+
+# Kimi / Moonshot
+KIMI_API_KEY=
+MOONSHOT_API_KEY=
+
+# Zhipu / GLM
+ZHIPU_API_KEY=
+GLM_API_KEY=
+
+# Ollama(OpenAI compatible)
+OLLAMA_HOST=http://localhost:11434/v1
+OLLAMA_API_KEY=ollama
+
+# vLLM(OpenAI compatible)
+VLLM_HOST=http://localhost:8000/v1
+VLLM_API_KEY=vllm
+
+
+# -------------------------------------------------
+# MCP(code_agent/agentic/code_agent.py)
+# -------------------------------------------------
+
+# 静默模式:关闭 MCP 注册等启动日志(TUI 默认会设为 1)
+# 可选值:1/0, true/false, yes/no
+CODE_AGENT_QUIET=1
+
+# Monitor:显式指定启动命令(需要你自己准备 monitor MCP server)
+# 例:MCP_MONITOR_COMMAND="/abs/path/to/mcp-monitor"
+MCP_MONITOR_COMMAND=
+
+# Playwright:显式指定启动命令
+# 例:MCP_PLAYWRIGHT_COMMAND="npx -y @playwright/mcp"
+MCP_PLAYWRIGHT_COMMAND=
+
+
+# -------------------------------------------------
+# Skills(OpenCode / Claude-style)
+# -------------------------------------------------
+
+# skills 根目录覆盖(默认会按以下标准路径扫描:.agents/skills, .opencode/skills, .claude/skills,
+# 以及全局 ~/.config/opencode/skills, ~/.claude/skills)
+CODE_AGENT_SKILLS_DIR=
+
+# 是否把 skills 的轻量索引(name/description)注入到每轮系统上下文(渐进式披露目录)
+CODE_AGENT_ENABLE_SKILLS_INDEX=1
+
+# 当用户意图明确是“去外部生态查找/安装 skills”时,是否自动加载 find-skills 的 SKILL.md 正文 SOP
+CODE_AGENT_AUTO_LOAD_FIND_SKILLS=1
+
+
+# -------------------------------------------------
+# TUI Logo(code_agent/hello_code_tui.py)
+# -------------------------------------------------
+
+# Logo 文件路径(png/jpg/gif)
+# 例:CODE_AGENT_LOGO="test/nailong.gif"
+CODE_AGENT_LOGO=
+
+# Logo 模式(三选一):
+# - image:渲染静态图片(gif 也只取首帧,半块字符)
+# - gif:渲染 gif 动图(半块字符)
+# - dot:把图片转成“彩色点阵”再显示(gif 取首帧)
+CODE_AGENT_LOGO_MODE=image
+
+# Logo 可见性:
+# - once:启动闪屏(默认;自动收起 + 首次输入也会收起)
+# - always:一直占位显示
+# - never:不显示
+CODE_AGENT_LOGO_VISIBILITY=once
+
+# once 模式下闪屏持续秒数(0.2 - 10.0)
+CODE_AGENT_LOGO_SPLASH_SECONDS=2
+
+# gif 模式是否播放动画(0/false/no 关闭;其它开启)
+CODE_AGENT_LOGO_ANIMATE=1
+
+# Logo 尺寸控制(可选)
+CODE_AGENT_LOGO_MAX_HEIGHT=
+CODE_AGENT_LOGO_DISABLE_HEIGHT_LIMIT=0
+CODE_AGENT_LOGO_WIDTH=
+
+# dot 模式微调
+CODE_AGENT_LOGO_DOT_CHAR=•
+CODE_AGENT_LOGO_DOT_ASPECT=0.55
+
+
+# -------------------------------------------------
+# 搜索后端(tools/builtin/search.py)
+# -------------------------------------------------
+
+TAVILY_API_KEY=
+SERPAPI_API_KEY=
+
+
+# -------------------------------------------------
+# 向量/记忆(Qdrant / Embedding / Perceptual)
+# -------------------------------------------------
+
+# Perceptual 模型名(可选)
+CLIP_MODEL=openai/clip-vit-base-patch32
+CLAP_MODEL=laion/clap-htsat-unfused
+
+# Qdrant
+QDRANT_URL=
+QDRANT_API_KEY=
+QDRANT_COLLECTION=hello_agents_vectors
+QDRANT_DISTANCE=cosine
+QDRANT_VECTOR_SIZE=384
+QDRANT_TIMEOUT=30
+
+# Qdrant 索引/检索参数(高级)
+QDRANT_HNSW_M=32
+QDRANT_HNSW_EF_CONSTRUCT=256
+QDRANT_SEARCH_EF=128
+QDRANT_SEARCH_EXACT=0
+
+# Embedding(memory/embedding.py)
+# 可选:dashscope / openai / local / ...
+EMBED_MODEL_TYPE=dashscope
+EMBED_MODEL_NAME=
+EMBED_API_KEY=
+EMBED_BASE_URL=
+
+
+# -------------------------------------------------
+# Neo4j(core/database_config.py)
+# -------------------------------------------------
+
+NEO4J_URI=bolt://localhost:7687
+NEO4J_USERNAME=neo4j
+NEO4J_PASSWORD=hello-agents-password
+NEO4J_DATABASE=neo4j
+NEO4J_MAX_CONNECTION_LIFETIME=3600
+NEO4J_MAX_CONNECTION_POOL_SIZE=50
+NEO4J_CONNECTION_TIMEOUT=60
+
diff --git a/Co-creation-projects/aug618-Praxis/images/cli.png b/Co-creation-projects/aug618-Praxis/images/cli.png
new file mode 100644
index 00000000..899cbf2f
Binary files /dev/null and b/Co-creation-projects/aug618-Praxis/images/cli.png differ
diff --git a/Co-creation-projects/aug618-Praxis/images/logo.png b/Co-creation-projects/aug618-Praxis/images/logo.png
new file mode 100644
index 00000000..a5e9e408
Binary files /dev/null and b/Co-creation-projects/aug618-Praxis/images/logo.png differ
diff --git a/Co-creation-projects/aug618-Praxis/images/nailong.gif b/Co-creation-projects/aug618-Praxis/images/nailong.gif
new file mode 100644
index 00000000..cdf3e9e3
Binary files /dev/null and b/Co-creation-projects/aug618-Praxis/images/nailong.gif differ
diff --git a/Co-creation-projects/aug618-Praxis/images/nl.png b/Co-creation-projects/aug618-Praxis/images/nl.png
new file mode 100644
index 00000000..08001961
Binary files /dev/null and b/Co-creation-projects/aug618-Praxis/images/nl.png differ
diff --git a/Co-creation-projects/aug618-Praxis/images/tui.gif b/Co-creation-projects/aug618-Praxis/images/tui.gif
new file mode 100644
index 00000000..82640805
Binary files /dev/null and b/Co-creation-projects/aug618-Praxis/images/tui.gif differ
diff --git a/Co-creation-projects/aug618-Praxis/memory/__init__.py b/Co-creation-projects/aug618-Praxis/memory/__init__.py
new file mode 100644
index 00000000..6ab9731c
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/__init__.py
@@ -0,0 +1,19 @@
+"""HelloAgents 记忆系统模块(轻量导出)
+
+注意:不要在此处 eager-import 所有记忆类型实现(semantic/perceptual 可能依赖额外第三方库)。
+需要某个记忆类型时,由 MemoryManager 在运行时按 enable_* 选项惰性加载。
+"""
+
+from .base import MemoryItem, MemoryConfig, BaseMemory
+from .manager import MemoryManager
+from .storage.document_store import DocumentStore, SQLiteDocumentStore
+
+__all__ = [
+ "MemoryManager",
+ "MemoryItem",
+ "MemoryConfig",
+ "BaseMemory",
+ "DocumentStore",
+ "SQLiteDocumentStore",
+]
+
diff --git a/Co-creation-projects/aug618-Praxis/memory/base.py b/Co-creation-projects/aug618-Praxis/memory/base.py
new file mode 100644
index 00000000..89082e9a
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/base.py
@@ -0,0 +1,174 @@
+"""记忆系统基础类和配置
+
+按照第8章架构设计的基础组件:
+- MemoryItem: 记忆项数据结构
+- MemoryConfig: 记忆系统配置
+- BaseMemory: 记忆基类
+"""
+
+from abc import ABC, abstractmethod
+from typing import List, Dict, Any
+from datetime import datetime
+from pydantic import BaseModel
+
+class MemoryItem(BaseModel):
+ """记忆项数据结构"""
+ id: str
+ content: str
+ memory_type: str
+ user_id: str
+ timestamp: datetime
+ importance: float = 0.5
+ metadata: Dict[str, Any] = {}
+
+ class Config:
+ arbitrary_types_allowed = True
+
+class MemoryConfig(BaseModel):
+ """记忆系统配置"""
+
+ # 存储路径
+ storage_path: str = "./memory_data"
+
+ # 向量/嵌入相关(可选关闭,适用于只用 SQLite 的轻量 episodic)
+ disable_vector_store: bool = False
+ disable_embeddings: bool = False
+
+ # 统计显示用的基础配置(仅用于展示)
+ max_capacity: int = 100
+ importance_threshold: float = 0.1
+ decay_factor: float = 0.95
+
+ # 工作记忆特定配置
+ working_memory_capacity: int = 10
+ working_memory_tokens: int = 2000
+ working_memory_ttl_minutes: int = 120
+
+ # 感知记忆特定配置
+ perceptual_memory_modalities: List[str] = ["text", "image", "audio", "video"]
+
+class BaseMemory(ABC):
+ """记忆基类
+
+ 定义所有记忆类型的通用接口和行为
+ """
+
+ def __init__(self, config: MemoryConfig, storage_backend=None):
+ self.config = config
+ self.storage = storage_backend
+ self.memory_type = self.__class__.__name__.lower().replace("memory", "")
+
+ @abstractmethod
+ def add(self, memory_item: MemoryItem) -> str:
+ """添加记忆项
+
+ Args:
+ memory_item: 记忆项对象
+
+ Returns:
+ 记忆ID
+ """
+ pass
+
+ @abstractmethod
+ def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
+ """检索相关记忆
+
+ Args:
+ query: 查询内容
+ limit: 返回数量限制
+ **kwargs: 其他检索参数
+
+ Returns:
+ 相关记忆列表
+ """
+ pass
+
+ @abstractmethod
+ def update(self, memory_id: str, content: str = None,
+ importance: float = None, metadata: Dict[str, Any] = None) -> bool:
+ """更新记忆
+
+ Args:
+ memory_id: 记忆ID
+ content: 新内容
+ importance: 新重要性
+ metadata: 新元数据
+
+ Returns:
+ 是否更新成功
+ """
+ pass
+
+ @abstractmethod
+ def remove(self, memory_id: str) -> bool:
+ """删除记忆
+
+ Args:
+ memory_id: 记忆ID
+
+ Returns:
+ 是否删除成功
+ """
+ pass
+
+ @abstractmethod
+ def has_memory(self, memory_id: str) -> bool:
+ """检查记忆是否存在
+
+ Args:
+ memory_id: 记忆ID
+
+ Returns:
+ 是否存在
+ """
+ pass
+
+ @abstractmethod
+ def clear(self):
+ """清空所有记忆"""
+ pass
+
+ @abstractmethod
+ def get_stats(self) -> Dict[str, Any]:
+ """获取记忆统计信息
+
+ Returns:
+ 统计信息字典
+ """
+ pass
+
+ def _generate_id(self) -> str:
+ """生成记忆ID"""
+ import uuid
+ return str(uuid.uuid4())
+
+ def _calculate_importance(self, content: str, base_importance: float = 0.5) -> float:
+ """计算记忆重要性
+
+ Args:
+ content: 记忆内容
+ base_importance: 基础重要性
+
+ Returns:
+ 计算后的重要性分数
+ """
+ importance = base_importance
+
+ # 基于内容长度
+ if len(content) > 100:
+ importance += 0.1
+
+ # 基于关键词
+ important_keywords = ["重要", "关键", "必须", "注意", "警告", "错误"]
+ if any(keyword in content for keyword in important_keywords):
+ importance += 0.2
+
+ return max(0.0, min(1.0, importance))
+
+ def __str__(self) -> str:
+ stats = self.get_stats()
+ return f"{self.__class__.__name__}(count={stats.get('count', 0)})"
+
+ def __repr__(self) -> str:
+ return self.__str__()
diff --git a/Co-creation-projects/aug618-Praxis/memory/embedding.py b/Co-creation-projects/aug618-Praxis/memory/embedding.py
new file mode 100644
index 00000000..2b405800
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/embedding.py
@@ -0,0 +1,315 @@
+"""统一嵌入模块(实现 + 提供器)
+
+说明(中文):
+- 提供统一的文本嵌入接口与多实现:本地Transformer、DashScope(通义千问)、TF-IDF兜底。
+- 暴露 get_text_embedder()/get_dimension()/refresh_embedder() 供各记忆类型统一使用。
+- 通过环境变量优先级:dashscope > local > tfidf。
+
+环境变量:
+- EMBED_MODEL_TYPE: "dashscope" | "local" | "tfidf"(默认 dashscope)
+- EMBED_MODEL_NAME: 模型名称(dashscope默认 text-embedding-v3;local默认 sentence-transformers/all-MiniLM-L6-v2)
+- EMBED_API_KEY: Embedding API Key(统一命名)
+- EMBED_BASE_URL: Embedding Base URL(统一命名,可选)
+"""
+
+from typing import List, Union, Optional
+import threading
+import os
+import numpy as np
+
+
+# ==============
+# 抽象与实现
+# ==============
+
+class EmbeddingModel:
+ """嵌入模型基类(最小接口)"""
+
+ def encode(self, texts: Union[str, List[str]]):
+ raise NotImplementedError
+
+ @property
+ def dimension(self) -> int:
+ raise NotImplementedError
+
+
+class LocalTransformerEmbedding(EmbeddingModel):
+ """本地Transformer嵌入(优先 sentence-transformers,缺失回退 transformers+torch)"""
+
+ def __init__(self, model_name: str = "sentence-transformers/all-MiniLM-L6-v2"):
+ self.model_name = model_name
+ self._backend = None # "st" 或 "hf"
+ self._st_model = None
+ self._hf_tokenizer = None
+ self._hf_model = None
+ self._dimension = None
+ self._load_backend()
+
+ def _load_backend(self):
+ # 优先 sentence-transformers
+ try:
+ from sentence_transformers import SentenceTransformer
+ self._st_model = SentenceTransformer(self.model_name)
+ test_vec = self._st_model.encode("test_text")
+ self._dimension = len(test_vec)
+ self._backend = "st"
+ return
+ except Exception:
+ self._st_model = None
+
+ # 回退 transformers
+ try:
+ from transformers import AutoTokenizer, AutoModel
+ import torch
+ self._hf_tokenizer = AutoTokenizer.from_pretrained(self.model_name)
+ self._hf_model = AutoModel.from_pretrained(self.model_name)
+ with torch.no_grad():
+ inputs = self._hf_tokenizer("test_text", return_tensors="pt", padding=True, truncation=True)
+ outputs = self._hf_model(**inputs)
+ test_embedding = outputs.last_hidden_state.mean(dim=1)
+ self._dimension = int(test_embedding.shape[1])
+ self._backend = "hf"
+ return
+ except Exception:
+ self._hf_tokenizer = None
+ self._hf_model = None
+
+ raise ImportError("未找到可用的本地嵌入后端,请安装 sentence-transformers 或 transformers+torch")
+
+ def encode(self, texts: Union[str, List[str]]):
+ if isinstance(texts, str):
+ inputs = [texts]
+ single = True
+ else:
+ inputs = list(texts)
+ single = False
+
+ if self._backend == "st":
+ vecs = self._st_model.encode(inputs)
+ if hasattr(vecs, "tolist"):
+ vecs = [v for v in vecs]
+ else:
+ import torch
+ tokenized = self._hf_tokenizer(inputs, return_tensors="pt", padding=True, truncation=True, max_length=512)
+ with torch.no_grad():
+ outputs = self._hf_model(**tokenized)
+ embeddings = outputs.last_hidden_state.mean(dim=1).cpu().numpy()
+ vecs = [v for v in embeddings]
+
+ if single:
+ return vecs[0]
+ return vecs
+
+ @property
+ def dimension(self) -> int:
+ return int(self._dimension or 0)
+
+
+class TFIDFEmbedding(EmbeddingModel):
+ """TF-IDF 简易兜底(在无深度模型时保证可用)"""
+
+ def __init__(self, max_features: int = 1000):
+ self.max_features = max_features
+ self._vectorizer = None
+ self._is_fitted = False
+ self._dimension = max_features
+ self._init_vectorizer()
+
+ def _init_vectorizer(self):
+ try:
+ from sklearn.feature_extraction.text import TfidfVectorizer
+ self._vectorizer = TfidfVectorizer(max_features=self.max_features, stop_words='english')
+ except ImportError:
+ raise ImportError("请安装 scikit-learn: pip install scikit-learn")
+
+ def fit(self, texts: List[str]):
+ self._vectorizer.fit(texts)
+ self._is_fitted = True
+ self._dimension = len(self._vectorizer.get_feature_names_out())
+
+ def encode(self, texts: Union[str, List[str]]):
+ if not self._is_fitted:
+ raise ValueError("TF-IDF模型未训练,请先调用fit()方法")
+ if isinstance(texts, str):
+ texts = [texts]
+ single = True
+ else:
+ single = False
+ tfidf_matrix = self._vectorizer.transform(texts)
+ embeddings = tfidf_matrix.toarray()
+ if single:
+ return embeddings[0]
+ return [e for e in embeddings]
+
+ @property
+ def dimension(self) -> int:
+ return self._dimension
+
+
+class DashScopeEmbedding(EmbeddingModel):
+ """阿里云 DashScope(通义千问)Embedding / OpenAI兼容REST 模式
+
+ 行为:
+ - 如提供 base_url,则优先使用 OpenAI 兼容的 REST 接口(POST {base_url}/embeddings)。
+ - 否则使用官方 dashscope SDK 的 TextEmbedding.call。
+ """
+
+ def __init__(self, model_name: str = "text-embedding-v3", api_key: Optional[str] = None, base_url: Optional[str] = None):
+ self.model_name = model_name
+ self.api_key = api_key
+ self.base_url = base_url
+ self._dimension = None
+ # 仅在非REST情况下初始化SDK
+ if not self.base_url:
+ self._init_client()
+ # 探测维度
+ test = self.encode("health_check")
+ self._dimension = len(test)
+
+ def _init_client(self):
+ try:
+ if self.api_key:
+ # 将统一命名的 API Key 注入到 SDK 期望的位置
+ os.environ["DASHSCOPE_API_KEY"] = self.api_key
+ import dashscope # noqa: F401
+ except ImportError:
+ raise ImportError("请安装 dashscope: pip install dashscope")
+
+ def encode(self, texts: Union[str, List[str]]):
+ if isinstance(texts, str):
+ inputs = [texts]
+ single = True
+ else:
+ inputs = list(texts)
+ single = False
+
+ # REST 模式(OpenAI兼容)
+ if self.base_url:
+ import requests
+ url = self.base_url.rstrip("/") + "/embeddings"
+ headers = {
+ "Authorization": f"Bearer {self.api_key}" if self.api_key else "",
+ "Content-Type": "application/json",
+ }
+ payload = {"model": self.model_name, "input": inputs}
+ resp = requests.post(url, headers=headers, json=payload, timeout=30)
+ if resp.status_code >= 400:
+ raise RuntimeError(f"Embedding REST 调用失败: {resp.status_code} {resp.text}")
+ data = resp.json()
+ # 期望结构:{"data": [{"embedding": [...]}]}
+ items = data.get("data") or []
+ vecs = [np.array(item.get("embedding")) for item in items]
+ if single:
+ return vecs[0]
+ return vecs
+
+ # SDK 模式
+ from dashscope import TextEmbedding
+ rsp = TextEmbedding.call(model=self.model_name, input=inputs)
+ embeddings_obj = None
+ if isinstance(rsp, dict):
+ embeddings_obj = (rsp.get("output") or {}).get("embeddings")
+ else:
+ embeddings_obj = getattr(getattr(rsp, "output", None), "embeddings", None)
+ if not embeddings_obj:
+ raise RuntimeError("DashScope 返回为空或格式不匹配")
+ vecs = [np.array(item.get("embedding") or item.get("vector")) for item in embeddings_obj]
+ if single:
+ return vecs[0]
+ return vecs
+
+ @property
+ def dimension(self) -> int:
+ return int(self._dimension or 0)
+
+
+# ==============
+# 工厂与回退
+# ==============
+
+def create_embedding_model(model_type: str = "local", **kwargs) -> EmbeddingModel:
+ """创建嵌入模型实例
+
+ model_type: "dashscope" | "local" | "tfidf"
+ kwargs: model_name, api_key
+ """
+ if model_type in ("local", "sentence_transformer", "huggingface"):
+ return LocalTransformerEmbedding(**kwargs)
+ elif model_type == "dashscope":
+ return DashScopeEmbedding(**kwargs)
+ elif model_type == "tfidf":
+ return TFIDFEmbedding(**kwargs)
+ else:
+ raise ValueError(f"不支持的模型类型: {model_type}")
+
+
+def create_embedding_model_with_fallback(preferred_type: str = "dashscope", **kwargs) -> EmbeddingModel:
+ """带回退的创建:dashscope -> local -> tfidf"""
+ if preferred_type in ("sentence_transformer", "huggingface"):
+ preferred_type = "local"
+ fallback = ["dashscope", "local", "tfidf"]
+ # 将首选放最前
+ if preferred_type in fallback:
+ fallback.remove(preferred_type)
+ fallback.insert(0, preferred_type)
+ for t in fallback:
+ try:
+ return create_embedding_model(t, **kwargs)
+ except Exception:
+ continue
+ raise RuntimeError("所有嵌入模型都不可用,请安装依赖或检查配置")
+
+
+# ==================
+# Provider(单例)
+# ==================
+
+_lock = threading.RLock()
+_embedder: Optional[EmbeddingModel] = None
+
+
+def _build_embedder() -> EmbeddingModel:
+ preferred = os.getenv("EMBED_MODEL_TYPE", "dashscope").strip()
+ # 根据提供商选择默认模型
+ default_model = "text-embedding-v3" if preferred == "dashscope" else "sentence-transformers/all-MiniLM-L6-v2"
+ model_name = os.getenv("EMBED_MODEL_NAME", default_model).strip()
+ kwargs = {}
+ if model_name:
+ kwargs["model_name"] = model_name
+ # 仅使用统一命名
+ api_key = os.getenv("EMBED_API_KEY")
+ if api_key:
+ kwargs["api_key"] = api_key
+ base_url = os.getenv("EMBED_BASE_URL")
+ if base_url:
+ kwargs["base_url"] = base_url
+ return create_embedding_model_with_fallback(preferred_type=preferred, **kwargs)
+
+
+def get_text_embedder() -> EmbeddingModel:
+ """获取全局共享的文本嵌入实例(线程安全单例)"""
+ global _embedder
+ if _embedder is not None:
+ return _embedder
+ with _lock:
+ if _embedder is None:
+ _embedder = _build_embedder()
+ return _embedder
+
+
+def get_dimension(default: int = 384) -> int:
+ """获取统一向量维度(失败回退默认值)"""
+ try:
+ return int(getattr(get_text_embedder(), "dimension", default))
+ except Exception:
+ return int(default)
+
+
+def refresh_embedder() -> EmbeddingModel:
+ """强制重建嵌入实例(可用于动态切换环境变量)"""
+ global _embedder
+ with _lock:
+ _embedder = _build_embedder()
+ return _embedder
+
+
diff --git a/Co-creation-projects/aug618-Praxis/memory/manager.py b/Co-creation-projects/aug618-Praxis/memory/manager.py
new file mode 100644
index 00000000..ae753fc5
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/manager.py
@@ -0,0 +1,342 @@
+"""记忆管理器 - 记忆核心层的统一管理接口"""
+
+from typing import List, Dict, Any, Optional, Union
+from datetime import datetime
+import uuid
+import logging
+
+from .base import MemoryItem, MemoryConfig
+# 存储和检索功能已被各记忆类型内部实现替代
+
+logger = logging.getLogger(__name__)
+
+class MemoryManager:
+ """记忆管理器 - 统一的记忆操作接口
+
+ 负责:
+ - 记忆生命周期管理
+ - 记忆优先级和重要性评估
+ - 记忆遗忘和清理机制
+ - 多类型记忆的协调管理
+ """
+
+ def __init__(
+ self,
+ config: Optional[MemoryConfig] = None,
+ user_id: str = "default_user",
+ enable_working: bool = True,
+ enable_episodic: bool = True,
+ enable_semantic: bool = True,
+ enable_perceptual: bool = False
+ ):
+ self.config = config or MemoryConfig()
+ self.user_id = user_id
+
+ # 存储和检索功能已移至各记忆类型内部实现
+
+ # 初始化各类型记忆
+ self.memory_types = {}
+
+ if enable_working:
+ from .types.working import WorkingMemory
+ self.memory_types['working'] = WorkingMemory(self.config)
+
+ if enable_episodic:
+ from .types.episodic import EpisodicMemory
+ self.memory_types['episodic'] = EpisodicMemory(self.config)
+
+ if enable_semantic:
+ from .types.semantic import SemanticMemory
+ self.memory_types['semantic'] = SemanticMemory(self.config)
+
+ if enable_perceptual:
+ from .types.perceptual import PerceptualMemory
+ self.memory_types['perceptual'] = PerceptualMemory(self.config)
+
+ logger.info(f"MemoryManager初始化完成,启用记忆类型: {list(self.memory_types.keys())}")
+
+ def add_memory(
+ self,
+ content: str,
+ memory_type: str = "working",
+ importance: Optional[float] = None,
+ metadata: Optional[Dict[str, Any]] = None,
+ auto_classify: bool = True
+ ) -> str:
+ """添加记忆
+
+ Args:
+ content: 记忆内容
+ memory_type: 记忆类型
+ importance: 重要性分数 (0-1)
+ metadata: 元数据
+ auto_classify: 是否自动分类到合适的记忆类型
+
+ Returns:
+ 记忆ID
+ """
+ # 自动分类记忆类型
+ if auto_classify:
+ memory_type = self._classify_memory_type(content, metadata)
+
+ # 计算重要性
+ if importance is None:
+ importance = self._calculate_importance(content, metadata)
+
+ # 创建记忆项
+ memory_item = MemoryItem(
+ id=str(uuid.uuid4()),
+ content=content,
+ memory_type=memory_type,
+ user_id=self.user_id,
+ timestamp=datetime.now(),
+ importance=importance,
+ metadata=metadata or {}
+ )
+
+ # 添加到对应的记忆类型
+ if memory_type in self.memory_types:
+ memory_id = self.memory_types[memory_type].add(memory_item)
+ logger.debug(f"添加记忆到 {memory_type}: {memory_id}")
+ return memory_id
+ else:
+ raise ValueError(f"不支持的记忆类型: {memory_type}")
+
+ def retrieve_memories(
+ self,
+ query: str,
+ memory_types: Optional[List[str]] = None,
+ limit: int = 10,
+ min_importance: float = 0.0,
+ time_range: Optional[tuple] = None
+ ) -> List[MemoryItem]:
+ """检索记忆
+
+ Args:
+ query: 查询内容
+ memory_types: 要检索的记忆类型列表
+ limit: 返回数量限制
+ min_importance: 最小重要性阈值
+ time_range: 时间范围 (start_time, end_time)
+
+ Returns:
+ 检索到的记忆列表
+ """
+ if memory_types is None:
+ memory_types = list(self.memory_types.keys())
+
+ # 从各个记忆类型中检索
+ all_results = []
+ per_type_limit = max(1, limit // len(memory_types))
+
+ for memory_type in memory_types:
+ if memory_type in self.memory_types:
+ memory_instance = self.memory_types[memory_type]
+ try:
+ # 使用各个记忆类型自己的检索方法
+ type_results = memory_instance.retrieve(
+ query=query,
+ limit=per_type_limit,
+ min_importance=min_importance,
+ user_id=self.user_id
+ )
+ all_results.extend(type_results)
+ except Exception as e:
+ logger.warning(f"检索 {memory_type} 记忆时出错: {e}")
+ continue
+
+ # 按重要性和相关性排序
+ all_results.sort(key=lambda x: x.importance, reverse=True)
+ return all_results[:limit]
+
+ def update_memory(
+ self,
+ memory_id: str,
+ content: Optional[str] = None,
+ importance: Optional[float] = None,
+ metadata: Optional[Dict[str, Any]] = None
+ ) -> bool:
+ """更新记忆
+
+ Args:
+ memory_id: 记忆ID
+ content: 新内容
+ importance: 新重要性
+ metadata: 新元数据
+
+ Returns:
+ 是否更新成功
+ """
+ # 查找记忆所在的类型
+ for memory_type, memory_instance in self.memory_types.items():
+ if memory_instance.has_memory(memory_id):
+ return memory_instance.update(memory_id, content, importance, metadata)
+
+ logger.warning(f"未找到记忆: {memory_id}")
+ return False
+
+ def remove_memory(self, memory_id: str) -> bool:
+ """删除记忆
+
+ Args:
+ memory_id: 记忆ID
+
+ Returns:
+ 是否删除成功
+ """
+ for memory_type, memory_instance in self.memory_types.items():
+ if memory_instance.has_memory(memory_id):
+ return memory_instance.remove(memory_id)
+
+ logger.warning(f"未找到记忆: {memory_id}")
+ return False
+
+ def forget_memories(
+ self,
+ strategy: str = "importance_based",
+ threshold: float = 0.1,
+ max_age_days: int = 30
+ ) -> int:
+ """记忆遗忘机制
+
+ Args:
+ strategy: 遗忘策略 ("importance_based", "time_based", "capacity_based")
+ threshold: 遗忘阈值
+ max_age_days: 最大保存天数
+
+ Returns:
+ 遗忘的记忆数量
+ """
+ total_forgotten = 0
+
+ for memory_type, memory_instance in self.memory_types.items():
+ if hasattr(memory_instance, 'forget'):
+ forgotten = memory_instance.forget(strategy, threshold, max_age_days)
+ total_forgotten += forgotten
+
+ logger.info(f"记忆遗忘完成: {total_forgotten} 条记忆")
+ return total_forgotten
+
+ def consolidate_memories(
+ self,
+ from_type: str = "working",
+ to_type: str = "episodic",
+ importance_threshold: float = 0.7
+ ) -> int:
+ """记忆整合 - 将重要的短期记忆转换为长期记忆
+
+ Args:
+ from_type: 源记忆类型
+ to_type: 目标记忆类型
+ importance_threshold: 重要性阈值
+
+ Returns:
+ 整合的记忆数量
+ """
+ if from_type not in self.memory_types or to_type not in self.memory_types:
+ logger.warning(f"记忆类型不存在: {from_type} -> {to_type}")
+ return 0
+
+ # 获取高重要性的源记忆
+ source_memory = self.memory_types[from_type]
+ target_memory = self.memory_types[to_type]
+
+ # 获取需要整合的记忆
+ all_memories = source_memory.get_all()
+ candidates = [
+ m for m in all_memories
+ if m.importance >= importance_threshold
+ ]
+
+ consolidated_count = 0
+ for memory in candidates:
+ # 移动到目标记忆类型
+ if source_memory.remove(memory.id):
+ memory.memory_type = to_type
+ memory.importance *= 1.1 # 提升重要性
+ target_memory.add(memory)
+ consolidated_count += 1
+
+ logger.info(f"记忆整合完成: {consolidated_count} 条记忆从 {from_type} 转移到 {to_type}")
+ return consolidated_count
+
+ def get_memory_stats(self) -> Dict[str, Any]:
+ """获取记忆统计信息"""
+ stats = {
+ "user_id": self.user_id,
+ "enabled_types": list(self.memory_types.keys()),
+ "total_memories": 0,
+ "memories_by_type": {},
+ "config": {
+ "max_capacity": self.config.max_capacity,
+ "importance_threshold": self.config.importance_threshold,
+ "decay_factor": self.config.decay_factor
+ }
+ }
+
+ for memory_type, memory_instance in self.memory_types.items():
+ type_stats = memory_instance.get_stats()
+ stats["memories_by_type"][memory_type] = type_stats
+ # 使用count字段(活跃记忆数),而不是total_count(包含已遗忘的)
+ stats["total_memories"] += type_stats.get("count", 0)
+
+ return stats
+
+ def clear_all_memories(self):
+ """清空所有记忆"""
+ for memory_type, memory_instance in self.memory_types.items():
+ memory_instance.clear()
+ logger.info("所有记忆已清空")
+
+
+
+
+ def _classify_memory_type(self, content: str, metadata: Optional[Dict[str, Any]]) -> str:
+ """自动分类记忆类型"""
+ if metadata and metadata.get("type"):
+ return metadata["type"]
+
+ # 简单的分类逻辑,可以扩展为更复杂的分类器
+ if self._is_episodic_content(content):
+ return "episodic"
+ elif self._is_semantic_content(content):
+ return "semantic"
+ else:
+ return "working"
+
+ def _is_episodic_content(self, content: str) -> bool:
+ """判断是否为情景记忆内容"""
+ episodic_keywords = ["昨天", "今天", "明天", "上次", "记得", "发生", "经历"]
+ return any(keyword in content for keyword in episodic_keywords)
+
+ def _is_semantic_content(self, content: str) -> bool:
+ """判断是否为语义记忆内容"""
+ semantic_keywords = ["定义", "概念", "规则", "知识", "原理", "方法"]
+ return any(keyword in content for keyword in semantic_keywords)
+
+ def _calculate_importance(self, content: str, metadata: Optional[Dict[str, Any]]) -> float:
+ """计算记忆重要性"""
+ importance = 0.5 # 基础重要性
+
+ # 基于内容长度
+ if len(content) > 100:
+ importance += 0.1
+
+ # 基于关键词
+ important_keywords = ["重要", "关键", "必须", "注意", "警告", "错误"]
+ if any(keyword in content for keyword in important_keywords):
+ importance += 0.2
+
+ # 基于元数据
+ if metadata:
+ if metadata.get("priority") == "high":
+ importance += 0.3
+ elif metadata.get("priority") == "low":
+ importance -= 0.2
+
+ return max(0.0, min(1.0, importance))
+
+
+ def __str__(self) -> str:
+ stats = self.get_memory_stats()
+ return f"MemoryManager(user={self.user_id}, total={stats['total_memories']})"
diff --git a/Co-creation-projects/aug618-Praxis/memory/rag/__init__.py b/Co-creation-projects/aug618-Praxis/memory/rag/__init__.py
new file mode 100644
index 00000000..6c58fecf
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/rag/__init__.py
@@ -0,0 +1,65 @@
+"""RAG (检索增强生成) 模块
+
+合并了 GraphRAG 能力:
+- loader:文件加载/分块(含PDF、语言标注、去重)
+- embedding/cache:嵌入与SQLite缓存,默认哈希回退
+- vector search:Qdrant召回
+- rank/merge:融合排序与片段合并
+"""
+
+# 说明:原先的 .embeddings 已合并到上级目录的 memory/embedding.py
+# 这里做兼容导出,避免历史引用报错。
+from ..embedding import (
+ EmbeddingModel,
+ LocalTransformerEmbedding,
+ TFIDFEmbedding,
+ create_embedding_model,
+ create_embedding_model_with_fallback,
+)
+from .document import Document, DocumentProcessor
+from .pipeline import (
+ load_and_chunk_texts,
+ build_graph_from_chunks,
+ index_chunks,
+ embed_query,
+ search_vectors,
+ rank,
+ merge_snippets,
+ rerank_with_cross_encoder,
+ expand_neighbors_from_pool,
+ compute_graph_signals_from_pool,
+ merge_snippets_grouped,
+ search_vectors_expanded,
+ compress_ranked_items,
+ tldr_summarize,
+)
+
+# 兼容旧类名(历史代码中可能从此处导入)
+SentenceTransformerEmbedding = LocalTransformerEmbedding
+HuggingFaceEmbedding = LocalTransformerEmbedding
+
+__all__ = [
+ "EmbeddingModel",
+ "LocalTransformerEmbedding",
+ "SentenceTransformerEmbedding", # 兼容别名
+ "HuggingFaceEmbedding", # 兼容别名
+ "TFIDFEmbedding",
+ "create_embedding_model",
+ "create_embedding_model_with_fallback",
+ "Document",
+ "DocumentProcessor",
+ "load_and_chunk_texts",
+ "build_graph_from_chunks",
+ "index_chunks",
+ "embed_query",
+ "search_vectors",
+ "rank",
+ "merge_snippets",
+ "rerank_with_cross_encoder",
+ "expand_neighbors_from_pool",
+ "compute_graph_signals_from_pool",
+ "merge_snippets_grouped",
+ "search_vectors_expanded",
+ "compress_ranked_items",
+ "tldr_summarize",
+]
diff --git a/Co-creation-projects/aug618-Praxis/memory/rag/document.py b/Co-creation-projects/aug618-Praxis/memory/rag/document.py
new file mode 100644
index 00000000..80d465d5
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/rag/document.py
@@ -0,0 +1,258 @@
+"""文档处理模块"""
+
+from typing import List, Dict, Any, Optional
+from dataclasses import dataclass
+from datetime import datetime
+import hashlib
+
+@dataclass
+class Document:
+ """文档类"""
+ content: str
+ metadata: Dict[str, Any]
+ doc_id: Optional[str] = None
+
+ def __post_init__(self):
+ if self.doc_id is None:
+ # 基于内容生成ID
+ self.doc_id = hashlib.md5(self.content.encode()).hexdigest()
+
+@dataclass
+class DocumentChunk:
+ """文档块类"""
+ content: str
+ metadata: Dict[str, Any]
+ chunk_id: Optional[str] = None
+ doc_id: Optional[str] = None
+ chunk_index: int = 0
+
+ def __post_init__(self):
+ if self.chunk_id is None:
+ # 基于文档ID和块索引生成ID
+ chunk_content = f"{self.doc_id}_{self.chunk_index}_{self.content[:50]}"
+ self.chunk_id = hashlib.md5(chunk_content.encode()).hexdigest()
+
+class DocumentProcessor:
+ """文档处理器"""
+
+ def __init__(
+ self,
+ chunk_size: int = 1000,
+ chunk_overlap: int = 200,
+ separators: Optional[List[str]] = None
+ ):
+ self.chunk_size = chunk_size
+ self.chunk_overlap = chunk_overlap
+ self.separators = separators or ["\n\n", "\n", "。", ".", " "]
+
+ def process_document(self, document: Document) -> List[DocumentChunk]:
+ """
+ 处理文档,分割成块
+
+ Args:
+ document: 输入文档
+
+ Returns:
+ 文档块列表
+ """
+ chunks = self._split_text(document.content)
+
+ document_chunks = []
+ for i, chunk_content in enumerate(chunks):
+ # 创建块的元数据
+ chunk_metadata = document.metadata.copy()
+ chunk_metadata.update({
+ "doc_id": document.doc_id,
+ "chunk_index": i,
+ "total_chunks": len(chunks),
+ "processed_at": datetime.now().isoformat()
+ })
+
+ chunk = DocumentChunk(
+ content=chunk_content,
+ metadata=chunk_metadata,
+ doc_id=document.doc_id,
+ chunk_index=i
+ )
+ document_chunks.append(chunk)
+
+ return document_chunks
+
+ def process_documents(self, documents: List[Document]) -> List[DocumentChunk]:
+ """
+ 批量处理文档
+
+ Args:
+ documents: 文档列表
+
+ Returns:
+ 所有文档块列表
+ """
+ all_chunks = []
+ for document in documents:
+ chunks = self.process_document(document)
+ all_chunks.extend(chunks)
+
+ return all_chunks
+
+ def _split_text(self, text: str) -> List[str]:
+ """
+ 分割文本为块
+
+ Args:
+ text: 输入文本
+
+ Returns:
+ 文本块列表
+ """
+ if len(text) <= self.chunk_size:
+ return [text]
+
+ chunks = []
+ start = 0
+
+ while start < len(text):
+ # 确定块的结束位置
+ end = start + self.chunk_size
+
+ if end >= len(text):
+ # 最后一块
+ chunks.append(text[start:])
+ break
+
+ # 寻找合适的分割点
+ split_point = self._find_split_point(text, start, end)
+
+ if split_point == -1:
+ # 没找到合适的分割点,强制分割
+ split_point = end
+
+ chunks.append(text[start:split_point])
+
+ # 计算下一块的开始位置(考虑重叠)
+ start = max(start + 1, split_point - self.chunk_overlap)
+
+ return chunks
+
+ def _find_split_point(self, text: str, start: int, end: int) -> int:
+ """
+ 在指定范围内寻找最佳分割点
+
+ Args:
+ text: 文本
+ start: 开始位置
+ end: 结束位置
+
+ Returns:
+ 分割点位置,-1表示未找到
+ """
+ # 从后往前寻找分隔符
+ for separator in self.separators:
+ # 在end附近寻找分隔符
+ search_start = max(start, end - 100) # 在最后100个字符中寻找
+
+ for i in range(end - len(separator), search_start - 1, -1):
+ if text[i:i + len(separator)] == separator:
+ return i + len(separator)
+
+ return -1
+
+ def merge_chunks(self, chunks: List[DocumentChunk], max_length: int = 2000) -> List[DocumentChunk]:
+ """
+ 合并小的文档块
+
+ Args:
+ chunks: 文档块列表
+ max_length: 合并后的最大长度
+
+ Returns:
+ 合并后的文档块列表
+ """
+ if not chunks:
+ return []
+
+ merged_chunks = []
+ current_chunk = chunks[0]
+
+ for next_chunk in chunks[1:]:
+ # 检查是否可以合并
+ combined_length = len(current_chunk.content) + len(next_chunk.content)
+
+ if (combined_length <= max_length and
+ current_chunk.doc_id == next_chunk.doc_id):
+ # 合并块
+ current_chunk.content += "\n" + next_chunk.content
+ current_chunk.metadata["total_chunks"] = current_chunk.metadata.get("total_chunks", 1) + 1
+ else:
+ # 不能合并,保存当前块
+ merged_chunks.append(current_chunk)
+ current_chunk = next_chunk
+
+ # 添加最后一个块
+ merged_chunks.append(current_chunk)
+
+ return merged_chunks
+
+ def filter_chunks(self, chunks: List[DocumentChunk], min_length: int = 50) -> List[DocumentChunk]:
+ """
+ 过滤太短的文档块
+
+ Args:
+ chunks: 文档块列表
+ min_length: 最小长度
+
+ Returns:
+ 过滤后的文档块列表
+ """
+ return [chunk for chunk in chunks if len(chunk.content.strip()) >= min_length]
+
+ def add_chunk_metadata(self, chunks: List[DocumentChunk], metadata: Dict[str, Any]) -> List[DocumentChunk]:
+ """
+ 为文档块添加额外元数据
+
+ Args:
+ chunks: 文档块列表
+ metadata: 要添加的元数据
+
+ Returns:
+ 更新后的文档块列表
+ """
+ for chunk in chunks:
+ chunk.metadata.update(metadata)
+
+ return chunks
+
+def load_text_file(file_path: str, encoding: str = "utf-8") -> Document:
+ """
+ 加载文本文件为文档
+
+ Args:
+ file_path: 文件路径
+ encoding: 文件编码
+
+ Returns:
+ 文档对象
+ """
+ with open(file_path, 'r', encoding=encoding) as f:
+ content = f.read()
+
+ metadata = {
+ "source": file_path,
+ "type": "text_file",
+ "loaded_at": datetime.now().isoformat()
+ }
+
+ return Document(content=content, metadata=metadata)
+
+def create_document(content: str, **metadata) -> Document:
+ """
+ 创建文档的便捷函数
+
+ Args:
+ content: 文档内容
+ **metadata: 元数据
+
+ Returns:
+ 文档对象
+ """
+ return Document(content=content, metadata=metadata)
diff --git a/Co-creation-projects/aug618-Praxis/memory/rag/pipeline.py b/Co-creation-projects/aug618-Praxis/memory/rag/pipeline.py
new file mode 100644
index 00000000..73faa0d2
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/rag/pipeline.py
@@ -0,0 +1,1207 @@
+from typing import List, Dict, Optional, Any
+import os
+import hashlib
+import sqlite3
+import time
+import json
+from ..embedding import get_text_embedder, get_dimension
+from ..storage.qdrant_store import QdrantVectorStore
+
+
+def _get_markitdown_instance():
+ """
+ Get a configured MarkItDown instance for document conversion.
+ """
+ try:
+ from markitdown import MarkItDown
+ return MarkItDown()
+ except ImportError:
+ print("[WARNING] MarkItDown not available. Install with: pip install markitdown")
+ return None
+
+
+def _is_markitdown_supported_format(path: str) -> bool:
+ """
+ Check if the file format is supported by MarkItDown.
+ Supports: PDF, Office docs (docx, xlsx, pptx), images (jpg, png, gif, bmp, tiff),
+ audio (mp3, wav, m4a), HTML, text formats (txt, md, csv, json, xml), ZIP files, etc.
+ """
+ ext = (os.path.splitext(path)[1] or '').lower()
+ supported_formats = {
+ # Documents
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
+ # Text formats
+ '.txt', '.md', '.csv', '.json', '.xml', '.html', '.htm',
+ # Images (OCR + metadata)
+ '.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif', '.webp',
+ # Audio (transcription + metadata)
+ '.mp3', '.wav', '.m4a', '.aac', '.flac', '.ogg',
+ # Archives
+ '.zip', '.tar', '.gz', '.rar',
+ # Code files
+ '.py', '.js', '.ts', '.java', '.cpp', '.c', '.h', '.css', '.scss',
+ # Other text
+ '.log', '.conf', '.ini', '.cfg', '.yaml', '.yml', '.toml'
+ }
+ return ext in supported_formats
+
+
+def _convert_to_markdown(path: str) -> str:
+ """
+ Universal document reader using MarkItDown with enhanced PDF processing.
+ Converts any supported file format to markdown text.
+ """
+ if not os.path.exists(path):
+ return ""
+
+ # 对PDF文件使用增强处理
+ ext = (os.path.splitext(path)[1] or '').lower()
+ if ext == '.pdf':
+ return _enhanced_pdf_processing(path)
+
+ # 其他格式使用原有MarkItDown
+ md_instance = _get_markitdown_instance()
+ if md_instance is None:
+ return _fallback_text_reader(path)
+
+ try:
+ result = md_instance.convert(path)
+ text = getattr(result, "text_content", None)
+ if isinstance(text, str) and text.strip():
+ return text
+ return ""
+ except Exception as e:
+ print(f"[WARNING] MarkItDown failed for {path}: {e}")
+ return _fallback_text_reader(path)
+
+def _enhanced_pdf_processing(path: str) -> str:
+ """
+ Enhanced PDF processing with post-processing cleanup.
+ """
+ print(f"[RAG] Using enhanced PDF processing for: {path}")
+
+ # 使用原有MarkItDown提取
+ md_instance = _get_markitdown_instance()
+ if md_instance is None:
+ return _fallback_text_reader(path)
+
+ try:
+ result = md_instance.convert(path)
+ raw_text = getattr(result, "text_content", None)
+ if not raw_text or not raw_text.strip():
+ return ""
+
+ # 后处理:清理和重组文本
+ cleaned_text = _post_process_pdf_text(raw_text)
+ print(f"[RAG] PDF post-processing completed: {len(raw_text)} -> {len(cleaned_text)} chars")
+ return cleaned_text
+
+ except Exception as e:
+ print(f"[WARNING] Enhanced PDF processing failed for {path}: {e}")
+ return _fallback_text_reader(path)
+
+def _post_process_pdf_text(text: str) -> str:
+ """
+ Post-process PDF text to improve quality.
+ """
+ import re
+
+ # 1. 按行分割并清理
+ lines = text.splitlines()
+ cleaned_lines = []
+
+ for line in lines:
+ line = line.strip()
+ if not line:
+ continue
+
+ # 移除单个字符的行(通常是噪音)
+ if len(line) <= 2 and not line.isdigit():
+ continue
+
+ # 移除明显的页眉页脚噪音
+ if re.match(r'^\d+$', line): # 纯数字行(页码)
+ continue
+ if line.lower() in ['github', 'project', 'forks', 'stars', 'language']:
+ continue
+
+ cleaned_lines.append(line)
+
+ # 2. 智能合并短行
+ merged_lines = []
+ i = 0
+
+ while i < len(cleaned_lines):
+ current_line = cleaned_lines[i]
+
+ # 如果当前行很短,尝试与下一行合并
+ if len(current_line) < 60 and i + 1 < len(cleaned_lines):
+ next_line = cleaned_lines[i + 1]
+
+ # 合并条件:都是内容,不是标题
+ if (not current_line.endswith(':') and
+ not current_line.endswith(':') and
+ not current_line.startswith('#') and
+ not next_line.startswith('#') and
+ len(next_line) < 120):
+
+ merged_line = current_line + " " + next_line
+ merged_lines.append(merged_line)
+ i += 2 # 跳过下一行
+ continue
+
+ merged_lines.append(current_line)
+ i += 1
+
+ # 3. 重新组织段落
+ paragraphs = []
+ current_paragraph = []
+
+ for line in merged_lines:
+ # 检查是否是新段落的开始
+ if (line.startswith('#') or # 标题
+ line.endswith(':') or # 中文冒号结尾
+ line.endswith(':') or # 英文冒号结尾
+ len(line) > 150 or # 长句通常是段落开始
+ not current_paragraph): # 第一行
+
+ # 保存当前段落
+ if current_paragraph:
+ paragraphs.append(' '.join(current_paragraph))
+ current_paragraph = []
+
+ paragraphs.append(line)
+ else:
+ current_paragraph.append(line)
+
+ # 添加最后一个段落
+ if current_paragraph:
+ paragraphs.append(' '.join(current_paragraph))
+
+ return '\n\n'.join(paragraphs)
+
+
+def _fallback_text_reader(path: str) -> str:
+ """
+ Simple fallback reader for basic text files when MarkItDown is unavailable.
+ """
+ try:
+ with open(path, 'r', encoding='utf-8', errors='ignore') as f:
+ return f.read()
+ except Exception:
+ try:
+ with open(path, 'r', encoding='latin-1', errors='ignore') as f:
+ return f.read()
+ except Exception:
+ return ""
+
+
+def _detect_lang(sample: str) -> str:
+ try:
+ from langdetect import detect
+ return detect(sample[:1000]) if sample else "unknown"
+ except Exception:
+ return "unknown"
+
+
+def _is_cjk(ch: str) -> bool:
+ code = ord(ch)
+ return (
+ 0x4E00 <= code <= 0x9FFF or
+ 0x3400 <= code <= 0x4DBF or
+ 0x20000 <= code <= 0x2A6DF or
+ 0x2A700 <= code <= 0x2B73F or
+ 0x2B740 <= code <= 0x2B81F or
+ 0x2B820 <= code <= 0x2CEAF or
+ 0xF900 <= code <= 0xFAFF
+ )
+
+
+def _approx_token_len(text: str) -> int:
+ # 近似估计:CJK字符按1 token,其他按空白分词
+ cjk = sum(1 for ch in text if _is_cjk(ch))
+ non_cjk_tokens = len([t for t in text.split() if t])
+ return cjk + non_cjk_tokens
+
+
+def _split_paragraphs_with_headings(text: str) -> List[Dict]:
+ lines = text.splitlines()
+ heading_stack: List[str] = []
+ paragraphs: List[Dict] = []
+ buf: List[str] = []
+ char_pos = 0
+ def flush_buf(end_pos: int):
+ if not buf:
+ return
+ content = "\n".join(buf).strip()
+ if not content:
+ return
+ paragraphs.append({
+ "content": content,
+ "heading_path": " > ".join(heading_stack) if heading_stack else None,
+ "start": max(0, end_pos - len(content)),
+ "end": end_pos,
+ })
+ for ln in lines:
+ raw = ln
+ if raw.strip().startswith("#"):
+ # heading line
+ flush_buf(char_pos)
+ level = len(raw) - len(raw.lstrip('#'))
+ title = raw.lstrip('#').strip()
+ if level <= 0:
+ level = 1
+ if level <= len(heading_stack):
+ heading_stack = heading_stack[:level-1]
+ heading_stack.append(title)
+ char_pos += len(raw) + 1
+ continue
+ # paragraph accumulation
+ if raw.strip() == "":
+ flush_buf(char_pos)
+ buf = []
+ else:
+ buf.append(raw)
+ char_pos += len(raw) + 1
+ flush_buf(char_pos)
+ if not paragraphs:
+ paragraphs = [{"content": text, "heading_path": None, "start": 0, "end": len(text)}]
+ return paragraphs
+
+
+def _chunk_paragraphs(paragraphs: List[Dict], chunk_tokens: int, overlap_tokens: int) -> List[Dict]:
+ chunks: List[Dict] = []
+ cur: List[Dict] = []
+ cur_tokens = 0
+ i = 0
+ while i < len(paragraphs):
+ p = paragraphs[i]
+ p_tokens = _approx_token_len(p["content"]) or 1
+ if cur_tokens + p_tokens <= chunk_tokens or not cur:
+ cur.append(p)
+ cur_tokens += p_tokens
+ i += 1
+ else:
+ # emit current chunk
+ content = "\n\n".join(x["content"] for x in cur)
+ start = cur[0]["start"]
+ end = cur[-1]["end"]
+ heading_path = next((x["heading_path"] for x in reversed(cur) if x.get("heading_path")), None)
+ chunks.append({
+ "content": content,
+ "start": start,
+ "end": end,
+ "heading_path": heading_path,
+ })
+ # build overlap by keeping tail tokens
+ if overlap_tokens > 0 and cur:
+ kept: List[Dict] = []
+ kept_tokens = 0
+ for x in reversed(cur):
+ t = _approx_token_len(x["content"]) or 1
+ if kept_tokens + t > overlap_tokens:
+ break
+ kept.append(x)
+ kept_tokens += t
+ cur = list(reversed(kept))
+ cur_tokens = kept_tokens
+ else:
+ cur = []
+ cur_tokens = 0
+ if cur:
+ content = "\n\n".join(x["content"] for x in cur)
+ start = cur[0]["start"]
+ end = cur[-1]["end"]
+ heading_path = next((x["heading_path"] for x in reversed(cur) if x.get("heading_path")), None)
+ chunks.append({
+ "content": content,
+ "start": start,
+ "end": end,
+ "heading_path": heading_path,
+ })
+ return chunks
+
+
+def load_and_chunk_texts(paths: List[str], chunk_size: int = 800, chunk_overlap: int = 100, namespace: Optional[str] = None, source_label: str = "rag") -> List[Dict]:
+ """
+ Universal document loader and chunker using MarkItDown.
+ Converts all supported formats to markdown, then chunks intelligently.
+ """
+ print(f"[RAG] Universal loader start: files={len(paths)} chunk_size={chunk_size} overlap={chunk_overlap} ns={namespace or 'default'}")
+ chunks: List[Dict] = []
+ seen_hashes = set()
+
+ for path in paths:
+ if not os.path.exists(path):
+ print(f"[WARNING] File not found: {path}")
+ continue
+
+ print(f"[RAG] Processing: {path}")
+ ext = (os.path.splitext(path)[1] or '').lower()
+
+ # Convert to markdown using MarkItDown
+ markdown_text = _convert_to_markdown(path)
+ if not markdown_text.strip():
+ print(f"[WARNING] No content extracted from: {path}")
+ continue
+
+ lang = _detect_lang(markdown_text)
+ doc_id = hashlib.md5(f"{path}|{len(markdown_text)}".encode('utf-8')).hexdigest()
+
+ # Always use markdown-aware chunking for better structure preservation
+ para = _split_paragraphs_with_headings(markdown_text)
+ token_chunks = _chunk_paragraphs(para, chunk_tokens=max(1, chunk_size), overlap_tokens=max(0, chunk_overlap))
+
+ for ch in token_chunks:
+ content = ch["content"]
+ start = ch.get("start", 0)
+ end = ch.get("end", start + len(content))
+ norm = content.strip()
+ if not norm:
+ continue
+
+ content_hash = hashlib.md5(norm.encode('utf-8')).hexdigest()
+ if content_hash in seen_hashes:
+ continue
+ seen_hashes.add(content_hash)
+
+ chunk_id = hashlib.md5(f"{doc_id}|{start}|{end}|{content_hash}".encode('utf-8')).hexdigest()
+ chunks.append({
+ "id": chunk_id,
+ "content": content,
+ "metadata": {
+ "source_path": path,
+ "file_ext": ext,
+ "doc_id": doc_id,
+ "lang": lang,
+ "start": start,
+ "end": end,
+ "content_hash": content_hash,
+ "namespace": namespace or "default",
+ "source": source_label,
+ "external": True,
+ "heading_path": ch.get("heading_path"),
+ "format": "markdown", # Mark all content as markdown-processed
+ },
+ })
+
+ print(f"[RAG] Universal loader done: total_chunks={len(chunks)}")
+ return chunks
+
+
+def build_graph_from_chunks(neo4j, chunks: List[Dict]) -> None:
+ created_docs = set()
+ for ch in chunks:
+ mem_id = ch["id"]
+ meta = ch.get("metadata", {})
+ source_path = meta.get("source_path")
+ doc_id = meta.get("doc_id")
+ if doc_id and doc_id not in created_docs:
+ created_docs.add(doc_id)
+ try:
+ neo4j.add_entity(
+ entity_id=doc_id,
+ name=os.path.basename(source_path or doc_id),
+ entity_type="Document",
+ properties={"source_path": source_path, "lang": meta.get("lang")}
+ )
+ except Exception:
+ pass
+ try:
+ neo4j.add_entity(entity_id=mem_id, name=mem_id, entity_type="Memory", properties={
+ "source_path": source_path,
+ "doc_id": doc_id,
+ "start": meta.get("start"),
+ "end": meta.get("end"),
+ })
+ except Exception:
+ pass
+ if doc_id:
+ try:
+ neo4j.add_relationship(from_id=doc_id, to_id=mem_id, rel_type="HAS_CHUNK", properties={})
+ except Exception:
+ pass
+
+
+def _preprocess_markdown_for_embedding(text: str) -> str:
+ """
+ Preprocess markdown text for better embedding quality.
+ Removes excessive markup while preserving semantic content.
+ """
+ import re
+
+ # Remove markdown headers symbols but keep the text
+ text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
+
+ # Remove markdown links but keep the text
+ text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
+
+ # Remove markdown emphasis markers
+ text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text) # bold
+ text = re.sub(r'\*([^*]+)\*', r'\1', text) # italic
+ text = re.sub(r'`([^`]+)`', r'\1', text) # inline code
+
+ # Remove markdown code blocks but keep content
+ text = re.sub(r'```[^\n]*\n([\s\S]*?)```', r'\1', text)
+
+ # Remove excessive whitespace
+ text = re.sub(r'\n\s*\n', '\n\n', text)
+ text = re.sub(r'[ \t]+', ' ', text)
+
+ return text.strip()
+
+
+def _create_default_vector_store(dimension: int = None) -> QdrantVectorStore:
+ """
+ Create default Qdrant vector store with RAG-optimized settings.
+ 使用连接管理器避免重复连接。
+ """
+ if dimension is None:
+ dimension = get_dimension(384)
+
+ # Check for Qdrant configuration
+ qdrant_url = os.getenv("QDRANT_URL")
+ qdrant_api_key = os.getenv("QDRANT_API_KEY")
+
+ # 使用连接管理器
+ from ..storage.qdrant_store import QdrantConnectionManager
+ return QdrantConnectionManager.get_instance(
+ url=qdrant_url,
+ api_key=qdrant_api_key,
+ collection_name="hello_agents_rag_vectors",
+ vector_size=dimension,
+ distance="cosine"
+ )
+
+
+# Cache functions removed - using unified embedder with internal caching
+
+
+def index_chunks(
+ store = None,
+ chunks: List[Dict] = None,
+ cache_db: Optional[str] = None,
+ batch_size: int = 64,
+ rag_namespace: str = "default"
+) -> None:
+ """
+ Index markdown chunks with unified embedding and Qdrant storage.
+ Uses百炼 API with fallback to sentence-transformers.
+ """
+ if not chunks:
+ print("[RAG] No chunks to index")
+ return
+
+ # Use unified embedding from embedding module
+ embedder = get_text_embedder()
+ dimension = get_dimension(384)
+
+ # Create default Qdrant store if not provided
+ if store is None:
+ store = _create_default_vector_store(dimension)
+ print(f"[RAG] Created default Qdrant store with dimension {dimension}")
+
+ # Preprocess markdown texts for better embeddings
+ processed_texts = []
+ for c in chunks:
+ raw_content = c["content"]
+ processed_content = _preprocess_markdown_for_embedding(raw_content)
+ processed_texts.append(processed_content)
+
+ print(f"[RAG] Embedding start: total_texts={len(processed_texts)} batch_size={batch_size}")
+
+ # Batch encoding with unified embedder
+ vecs: List[List[float]] = []
+ for i in range(0, len(processed_texts), batch_size):
+ part = processed_texts[i:i+batch_size]
+ try:
+ # Use unified embedder directly (handles caching internally)
+ part_vecs = embedder.encode(part)
+
+ # Normalize to List[List[float]]
+ if not isinstance(part_vecs, list):
+ # 单个numpy数组转为列表中的列表
+ if hasattr(part_vecs, "tolist"):
+ part_vecs = [part_vecs.tolist()]
+ else:
+ part_vecs = [list(part_vecs)]
+ else:
+ # 检查是否是嵌套列表
+ if part_vecs and not isinstance(part_vecs[0], (list, tuple)) and hasattr(part_vecs[0], "__len__"):
+ # numpy数组列表 -> 转换每个数组
+ normalized_vecs = []
+ for v in part_vecs:
+ if hasattr(v, "tolist"):
+ normalized_vecs.append(v.tolist())
+ else:
+ normalized_vecs.append(list(v))
+ part_vecs = normalized_vecs
+ elif part_vecs and not isinstance(part_vecs[0], (list, tuple)):
+ # 单个向量被误判为列表,实际应该包装成[[...]]
+ if hasattr(part_vecs, "tolist"):
+ part_vecs = [part_vecs.tolist()]
+ else:
+ part_vecs = [list(part_vecs)]
+
+ for v in part_vecs:
+ try:
+ # 确保向量是float列表
+ if hasattr(v, "tolist"):
+ v = v.tolist()
+ v_norm = [float(x) for x in v]
+ if len(v_norm) != dimension:
+ print(f"[WARNING] 向量维度异常: 期望{dimension}, 实际{len(v_norm)}")
+ # 用零向量填充或截断
+ if len(v_norm) < dimension:
+ v_norm.extend([0.0] * (dimension - len(v_norm)))
+ else:
+ v_norm = v_norm[:dimension]
+ vecs.append(v_norm)
+ except Exception as e:
+ print(f"[WARNING] 向量转换失败: {e}, 使用零向量")
+ vecs.append([0.0] * dimension)
+
+ except Exception as e:
+ print(f"[WARNING] Batch {i} encoding failed: {e}")
+ print(f"[RAG] Retrying batch {i} with smaller chunks...")
+
+ # 尝试重试:将批次分解为更小的块
+ success = False
+ for j in range(0, len(part), 8): # 更小的批次
+ small_part = part[j:j+8]
+ try:
+ import time
+ time.sleep(2) # 等待2秒避免频率限制
+
+ small_vecs = embedder.encode(small_part)
+ # Normalize to List[List[float]]
+ if isinstance(small_vecs, list) and small_vecs and not isinstance(small_vecs[0], list):
+ small_vecs = [small_vecs]
+
+ for v in small_vecs:
+ if hasattr(v, "tolist"):
+ v = v.tolist()
+ try:
+ v_norm = [float(x) for x in v]
+ if len(v_norm) != dimension:
+ print(f"[WARNING] 向量维度异常: 期望{dimension}, 实际{len(v_norm)}")
+ if len(v_norm) < dimension:
+ v_norm.extend([0.0] * (dimension - len(v_norm)))
+ else:
+ v_norm = v_norm[:dimension]
+ vecs.append(v_norm)
+ success = True
+ except Exception as e2:
+ print(f"[WARNING] 小批次向量转换失败: {e2}")
+ vecs.append([0.0] * dimension)
+ except Exception as e2:
+ print(f"[WARNING] 小批次 {j//8} 仍然失败: {e2}")
+ # 为这个小批次创建零向量
+ for _ in range(len(small_part)):
+ vecs.append([0.0] * dimension)
+
+ if not success:
+ print(f"[ERROR] 批次 {i} 完全失败,使用零向量")
+
+ print(f"[RAG] Embedding progress: {min(i+batch_size, len(processed_texts))}/{len(processed_texts)}")
+
+ # Prepare metadata with RAG tags
+ metas: List[Dict] = []
+ ids: List[str] = []
+ for ch in chunks:
+ meta = {
+ "memory_id": ch["id"],
+ "user_id": "rag_user",
+ "memory_type": "rag_chunk",
+ "content": ch["content"], # Keep original markdown content
+ "data_source": "rag_pipeline", # RAG identification tag
+ "rag_namespace": rag_namespace,
+ "is_rag_data": True, # Clear RAG data marker
+ }
+ # Merge chunk metadata
+ meta.update(ch.get("metadata", {}))
+ metas.append(meta)
+ ids.append(ch["id"])
+
+ print(f"[RAG] Qdrant upsert start: n={len(vecs)}")
+ success = store.add_vectors(vectors=vecs, metadata=metas, ids=ids)
+ if success:
+ print(f"[RAG] Qdrant upsert done: {len(vecs)} vectors indexed")
+ else:
+ print(f"[RAG] Qdrant upsert failed")
+ raise RuntimeError("Failed to index vectors to Qdrant")
+
+
+def embed_query(query: str) -> List[float]:
+ """
+ Embed query using unified embedding (百炼 with fallback).
+ """
+ embedder = get_text_embedder()
+ dimension = get_dimension(384)
+ try:
+ vec = embedder.encode(query)
+
+ # Normalize to List[float]
+ if hasattr(vec, "tolist"):
+ vec = vec.tolist()
+
+ # 处理嵌套列表情况
+ if isinstance(vec, list) and vec and isinstance(vec[0], (list, tuple)):
+ vec = vec[0] # Extract first vector if nested
+
+ # 转换为float列表
+ result = [float(x) for x in vec]
+
+ # 检查维度
+ if len(result) != dimension:
+ print(f"[WARNING] Query向量维度异常: 期望{dimension}, 实际{len(result)}")
+ # 用零向量填充或截断
+ if len(result) < dimension:
+ result.extend([0.0] * (dimension - len(result)))
+ else:
+ result = result[:dimension]
+
+ return result
+ except Exception as e:
+ print(f"[WARNING] Query embedding failed: {e}")
+ # Return zero vector as fallback
+ return [0.0] * dimension
+
+
+def search_vectors(
+ store = None,
+ query: str = "",
+ top_k: int = 8,
+ rag_namespace: Optional[str] = None,
+ only_rag_data: bool = True,
+ score_threshold: Optional[float] = None
+) -> List[Dict]:
+ """
+ Search RAG vectors using unified embedding and Qdrant.
+ """
+ if not query:
+ return []
+
+ # Create default store if not provided
+ if store is None:
+ store = _create_default_vector_store()
+
+ # Embed query with unified embedder
+ qv = embed_query(query)
+
+ # Build filter for RAG data
+ where = {"memory_type": "rag_chunk"}
+ if only_rag_data:
+ where["is_rag_data"] = True
+ where["data_source"] = "rag_pipeline"
+ if rag_namespace:
+ where["rag_namespace"] = rag_namespace
+
+ try:
+ return store.search_similar(
+ query_vector=qv,
+ limit=top_k,
+ score_threshold=score_threshold,
+ where=where
+ )
+ except Exception as e:
+ print(f"[WARNING] RAG search failed: {e}")
+ return []
+
+
+def _prompt_mqe(query: str, n: int) -> List[str]:
+ try:
+ from core.llm import HelloAgentsLLM
+ llm = HelloAgentsLLM()
+ prompt = [
+ {"role": "system", "content": "你是检索查询扩展助手。生成语义等价或互补的多样化查询。使用中文,简短,避免标点。"},
+ {"role": "user", "content": f"原始查询:{query}\n请给出{n}个不同表述的查询,每行一个。"}
+ ]
+ text = llm.invoke(prompt)
+ lines = [ln.strip("- \t") for ln in (text or "").splitlines()]
+ outs = [ln for ln in lines if ln]
+ return outs[:n] or [query]
+ except Exception:
+ return [query]
+
+
+def _prompt_hyde(query: str) -> Optional[str]:
+ try:
+ from core.llm import HelloAgentsLLM
+ llm = HelloAgentsLLM()
+ prompt = [
+ {"role": "system", "content": "根据用户问题,先写一段可能的答案性段落,用于向量检索的查询文档(不要分析过程)。"},
+ {"role": "user", "content": f"问题:{query}\n请直接写一段中等长度、客观、包含关键术语的段落。"}
+ ]
+ return llm.invoke(prompt)
+ except Exception:
+ return None
+
+
+def search_vectors_expanded(
+ store = None,
+ query: str = "",
+ top_k: int = 8,
+ rag_namespace: Optional[str] = None,
+ only_rag_data: bool = True,
+ score_threshold: Optional[float] = None,
+ enable_mqe: bool = False,
+ mqe_expansions: int = 2,
+ enable_hyde: bool = False,
+ candidate_pool_multiplier: int = 4,
+) -> List[Dict]:
+ """
+ Search with query expansion using unified embedding and Qdrant.
+ """
+ if not query:
+ return []
+
+ # Create default store if not provided
+ if store is None:
+ store = _create_default_vector_store()
+
+ # expansions
+ expansions: List[str] = [query]
+
+ if enable_mqe and mqe_expansions > 0:
+ expansions.extend(_prompt_mqe(query, mqe_expansions))
+ if enable_hyde:
+ hyde_text = _prompt_hyde(query)
+ if hyde_text:
+ expansions.append(hyde_text)
+
+ # unique and trim
+ uniq: List[str] = []
+ for e in expansions:
+ if e and e not in uniq:
+ uniq.append(e)
+ expansions = uniq[: max(1, len(uniq))]
+
+ # distribute pool per expansion
+ pool = max(top_k * candidate_pool_multiplier, 20)
+ per = max(1, pool // max(1, len(expansions)))
+
+ # Build filter for RAG data
+ where = {"memory_type": "rag_chunk"}
+ if only_rag_data:
+ where["is_rag_data"] = True
+ where["data_source"] = "rag_pipeline"
+ if rag_namespace:
+ where["rag_namespace"] = rag_namespace
+
+ # collect hits across expansions
+ agg: Dict[str, Dict] = {}
+ for q in expansions:
+ qv = embed_query(q)
+ hits = store.search_similar(query_vector=qv, limit=per, score_threshold=score_threshold, where=where)
+ for h in hits:
+ mid = h.get("metadata", {}).get("memory_id", h.get("id"))
+ s = float(h.get("score", 0.0))
+ if mid not in agg or s > float(agg[mid].get("score", 0.0)):
+ agg[mid] = h
+ # return top by score
+ merged = list(agg.values())
+ merged.sort(key=lambda x: float(x.get("score", 0.0)), reverse=True)
+ return merged[:top_k]
+
+
+def _try_load_cross_encoder(model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2"):
+ try:
+ from sentence_transformers import CrossEncoder
+ return CrossEncoder(model_name)
+ except Exception:
+ return None
+
+
+def rerank_with_cross_encoder(query: str, items: List[Dict], model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2", top_k: int = 10) -> List[Dict]:
+ ce = _try_load_cross_encoder(model_name)
+ if ce is None or not items:
+ return items[:top_k]
+ pairs = [[query, it.get("content", "")] for it in items]
+ try:
+ scores = ce.predict(pairs)
+ for it, s in zip(items, scores):
+ it["rerank_score"] = float(s)
+ items.sort(key=lambda x: x.get("rerank_score", x.get("score", 0.0)), reverse=True)
+ return items[:top_k]
+ except Exception:
+ return items[:top_k]
+
+
+def compute_graph_signals_from_pool(vector_hits: List[Dict], same_doc_weight: float = 1.0, proximity_weight: float = 1.0, proximity_window_chars: int = 1600) -> Dict[str, float]:
+ """
+ Compute graph signals with direct parameters instead of environment variables.
+ """
+
+ # group by doc
+ by_doc: Dict[str, List[Dict]] = {}
+ for h in vector_hits:
+ meta = h.get("metadata", {})
+ did = meta.get("doc_id")
+ if not did:
+ # fall back to memory_id grouping if doc missing
+ did = meta.get("memory_id") or h.get("id")
+ by_doc.setdefault(did, []).append(h)
+
+ # same-doc density score
+ doc_counts = {d: len(arr) for d, arr in by_doc.items()}
+ max_count = max(doc_counts.values()) if doc_counts else 1
+
+ # proximity score per hit within same doc
+ graph_signal: Dict[str, float] = {}
+ for did, arr in by_doc.items():
+ arr.sort(key=lambda x: x.get("metadata", {}).get("start", 0))
+ # precompute density
+ density = doc_counts.get(did, 1) / max_count
+ # proximity accumulation
+ for i, h in enumerate(arr):
+ mid = h.get("metadata", {}).get("memory_id", h.get("id"))
+ pos_i = h.get("metadata", {}).get("start", 0)
+ prox_acc = 0.0
+ # look around neighbors within window
+ # two-pointer expansion
+ # left
+ j = i - 1
+ while j >= 0:
+ pos_j = arr[j].get("metadata", {}).get("start", 0)
+ dist = abs(pos_i - pos_j)
+ if dist > proximity_window_chars:
+ break
+ prox_acc += max(0.0, 1.0 - (dist / max(1.0, float(proximity_window_chars))))
+ j -= 1
+ # right
+ j = i + 1
+ while j < len(arr):
+ pos_j = arr[j].get("metadata", {}).get("start", 0)
+ dist = abs(pos_i - pos_j)
+ if dist > proximity_window_chars:
+ break
+ prox_acc += max(0.0, 1.0 - (dist / max(1.0, float(proximity_window_chars))))
+ j += 1
+ # combine
+ score = same_doc_weight * density + proximity_weight * prox_acc
+ graph_signal[mid] = graph_signal.get(mid, 0.0) + score
+
+ # normalize to [0,1]
+ if graph_signal:
+ max_v = max(graph_signal.values())
+ if max_v > 0:
+ for k in list(graph_signal.keys()):
+ graph_signal[k] = graph_signal[k] / max_v
+ return graph_signal
+
+
+def rank(vector_hits: List[Dict], graph_signals: Optional[Dict[str, float]] = None, w_vector: float = 0.7, w_graph: float = 0.3) -> List[Dict]:
+ """
+ Rank results with direct weight parameters instead of environment variables.
+ """
+ items: List[Dict] = []
+ graph_signals = graph_signals or {}
+ for h in vector_hits:
+ mid = h.get("metadata", {}).get("memory_id", h.get("id"))
+ g = float(graph_signals.get(mid, 0.0))
+ v = float(h.get("score", 0.0))
+ score = w_vector * v + w_graph * g
+ items.append({
+ "memory_id": mid,
+ "score": score,
+ "vector_score": v,
+ "graph_score": g,
+ "content": h.get("metadata", {}).get("content", ""),
+ "metadata": h.get("metadata", {}),
+ })
+ items.sort(key=lambda x: x["score"], reverse=True)
+ return items
+
+
+def merge_snippets(ranked_items: List[Dict], max_chars: int = 1200) -> str:
+ out: List[str] = []
+ total = 0
+ for it in ranked_items:
+ text = it.get("content", "").strip()
+ if not text:
+ continue
+ if total + len(text) > max_chars:
+ remain = max_chars - total
+ if remain <= 0:
+ break
+ out.append(text[:remain])
+ total += remain
+ break
+ out.append(text)
+ total += len(text)
+ return "\n\n".join(out)
+
+
+def expand_neighbors_from_pool(selected: List[Dict], pool: List[Dict], neighbors: int = 1, max_additions: int = 5) -> List[Dict]:
+ if not selected or not pool or neighbors <= 0:
+ return selected
+ # index pool by doc_id and sort by start
+ by_doc: Dict[str, List[Dict]] = {}
+ for it in pool:
+ meta = it.get("metadata", {})
+ did = meta.get("doc_id")
+ if not did:
+ continue
+ by_doc.setdefault(did, []).append(it)
+ for did, arr in by_doc.items():
+ arr.sort(key=lambda x: (x.get("metadata", {}).get("start", 0)))
+ selected_ids = set(it.get("memory_id") for it in selected)
+ additions: List[Dict] = []
+ for it in selected:
+ meta = it.get("metadata", {})
+ did = meta.get("doc_id")
+ if not did or did not in by_doc:
+ continue
+ arr = by_doc[did]
+ # find index
+ try:
+ idx = next(i for i, x in enumerate(arr) if x.get("memory_id") == it.get("memory_id"))
+ except StopIteration:
+ continue
+ for offset in range(1, neighbors + 1):
+ for j in (idx - offset, idx + offset):
+ if 0 <= j < len(arr):
+ cand = arr[j]
+ mid = cand.get("memory_id")
+ if mid not in selected_ids:
+ additions.append(cand)
+ selected_ids.add(mid)
+ if len(additions) >= max_additions:
+ break
+ if len(additions) >= max_additions:
+ break
+ if len(additions) >= max_additions:
+ break
+ # keep relative order by score
+ extended = list(selected) + additions
+ extended.sort(key=lambda x: (x.get("rerank_score", x.get("score", 0.0))), reverse=True)
+ return extended
+
+
+def merge_snippets_grouped(ranked_items: List[Dict], max_chars: int = 1200, include_citations: bool = True) -> str:
+ # Group by doc_id and aggregate doc score
+ by_doc: Dict[str, List[Dict]] = {}
+ doc_score: Dict[str, float] = {}
+ for it in ranked_items:
+ meta = it.get("metadata", {})
+ did = meta.get("doc_id") or meta.get("source_path") or "unknown"
+ by_doc.setdefault(did, []).append(it)
+ doc_score[did] = doc_score.get(did, 0.0) + float(it.get("score", 0.0))
+ # Sort docs by aggregate score
+ ordered_docs = sorted(by_doc.keys(), key=lambda d: doc_score.get(d, 0.0), reverse=True)
+ # Within doc, order by start offset to preserve context
+ for d in ordered_docs:
+ by_doc[d].sort(key=lambda x: (x.get("metadata", {}).get("start", 0)))
+ out: List[str] = []
+ citations: List[Dict] = []
+ total = 0
+ cite_index = 1
+ for did in ordered_docs:
+ parts = by_doc[did]
+ for it in parts:
+ text = (it.get("content", "") or "").strip()
+ if not text:
+ continue
+ # add citation marker if enabled
+ suffix = ""
+ if include_citations:
+ suffix = f" [{cite_index}]"
+ need = len(text) + (len(suffix) if suffix else 0)
+ if total + need > max_chars:
+ remain = max_chars - total
+ if remain <= 0:
+ break
+ clipped = text[: max(0, remain - len(suffix))]
+ if clipped:
+ out.append(clipped + suffix)
+ total += len(clipped) + len(suffix)
+ if include_citations:
+ m = it.get("metadata", {})
+ citations.append({
+ "index": cite_index,
+ "source_path": m.get("source_path"),
+ "doc_id": m.get("doc_id"),
+ "start": m.get("start"),
+ "end": m.get("end"),
+ "heading_path": m.get("heading_path"),
+ })
+ cite_index += 1
+ break
+ out.append(text + suffix)
+ total += need
+ if include_citations:
+ m = it.get("metadata", {})
+ citations.append({
+ "index": cite_index,
+ "source_path": m.get("source_path"),
+ "doc_id": m.get("doc_id"),
+ "start": m.get("start"),
+ "end": m.get("end"),
+ "heading_path": m.get("heading_path"),
+ })
+ cite_index += 1
+ if total >= max_chars:
+ break
+ merged = "\n\n".join(out)
+ if include_citations and citations:
+ lines: List[str] = [merged, "", "References:"]
+ for c in citations:
+ loc = ""
+ if c.get("start") is not None and c.get("end") is not None:
+ loc = f" ({c['start']}-{c['end']})"
+ hp = f" – {c['heading_path']}" if c.get("heading_path") else ""
+ sp = c.get("source_path") or c.get("doc_id") or "source"
+ lines.append(f"[{c['index']}] {sp}{loc}{hp}")
+ return "\n".join(lines)
+ return merged
+
+
+def compress_ranked_items(ranked_items: List[Dict], enable_compression: bool = True, max_per_doc: int = 2, join_gap: int = 200) -> List[Dict]:
+ """
+ Compress ranked items with direct parameters instead of environment variables.
+ """
+ if not enable_compression:
+ return ranked_items
+ by_doc_count: Dict[str, int] = {}
+ last_by_doc: Dict[str, Dict] = {}
+ new_items: List[Dict] = []
+ for it in ranked_items:
+ meta = it.get("metadata", {})
+ did = meta.get("doc_id") or meta.get("source_path") or "unknown"
+ start = int(meta.get("start") or 0)
+ end = int(meta.get("end") or (start + len(it.get("content", "") or "")))
+ if did not in last_by_doc:
+ last_by_doc[did] = it
+ by_doc_count[did] = 1
+ new_items.append(it)
+ continue
+ last = last_by_doc[did]
+ lmeta = last.get("metadata", {})
+ lstart = int(lmeta.get("start") or 0)
+ lend = int(lmeta.get("end") or (lstart + len(last.get("content", "") or "")))
+ if start - lend <= join_gap and start >= lstart:
+ # merge into last
+ merged_text = (last.get("content", "") or "").strip()
+ add_text = (it.get("content", "") or "").strip()
+ if add_text:
+ if merged_text:
+ merged_text = merged_text + "\n\n" + add_text
+ else:
+ merged_text = add_text
+ last["content"] = merged_text
+ lmeta["end"] = max(lend, end)
+ # keep the higher score
+ try:
+ last["score"] = max(float(last.get("score", 0.0)), float(it.get("score", 0.0)))
+ except Exception:
+ pass
+ last_by_doc[did] = last
+ else:
+ cnt = by_doc_count.get(did, 0)
+ if cnt >= max_per_doc:
+ continue
+ new_items.append(it)
+ last_by_doc[did] = it
+ by_doc_count[did] = cnt + 1
+ return new_items
+
+
+def tldr_summarize(text: str, bullets: int = 3) -> Optional[str]:
+ try:
+ if not text or len(text.strip()) == 0:
+ return None
+ from core.llm import HelloAgentsLLM
+ llm = HelloAgentsLLM()
+ prompt = [
+ {"role": "system", "content": "请将以下内容概括为简洁的要点列表(最多3-5条),用中文,避免重复,突出关键信息。"},
+ {"role": "user", "content": f"请用 {max(1, min(5, int(bullets)))} 条要点总结:\n\n{text}"},
+ ]
+ out = llm.invoke(prompt)
+ return out
+ except Exception:
+ return None
+
+
+# ==================
+# High-level RAG Pipeline API
+# ==================
+
+def create_rag_pipeline(
+ qdrant_url: Optional[str] = None,
+ qdrant_api_key: Optional[str] = None,
+ collection_name: str = "hello_agents_rag_vectors",
+ rag_namespace: str = "default"
+) -> Dict[str, Any]:
+ """
+ Create a complete RAG pipeline with Qdrant and unified embedding.
+
+ Returns:
+ Dict containing store, namespace, and helper functions
+ """
+ dimension = get_dimension(384)
+
+ store = QdrantVectorStore(
+ url=qdrant_url,
+ api_key=qdrant_api_key,
+ collection_name=collection_name,
+ vector_size=dimension,
+ distance="cosine"
+ )
+
+ def add_documents(file_paths: List[str], chunk_size: int = 800, chunk_overlap: int = 100):
+ """Add documents to RAG pipeline"""
+ chunks = load_and_chunk_texts(
+ paths=file_paths,
+ chunk_size=chunk_size,
+ chunk_overlap=chunk_overlap,
+ namespace=rag_namespace,
+ source_label="rag"
+ )
+ index_chunks(
+ store=store,
+ chunks=chunks,
+ rag_namespace=rag_namespace
+ )
+ return len(chunks)
+
+ def search(query: str, top_k: int = 8, score_threshold: Optional[float] = None):
+ """Search RAG knowledge base"""
+ return search_vectors(
+ store=store,
+ query=query,
+ top_k=top_k,
+ rag_namespace=rag_namespace,
+ score_threshold=score_threshold
+ )
+
+ def search_advanced(
+ query: str,
+ top_k: int = 8,
+ enable_mqe: bool = False,
+ enable_hyde: bool = False,
+ score_threshold: Optional[float] = None
+ ):
+ """Advanced search with query expansion"""
+ return search_vectors_expanded(
+ store=store,
+ query=query,
+ top_k=top_k,
+ rag_namespace=rag_namespace,
+ enable_mqe=enable_mqe,
+ enable_hyde=enable_hyde,
+ score_threshold=score_threshold
+ )
+
+ def get_stats():
+ """Get pipeline statistics"""
+ return store.get_collection_stats()
+
+ return {
+ "store": store,
+ "namespace": rag_namespace,
+ "add_documents": add_documents,
+ "search": search,
+ "search_advanced": search_advanced,
+ "get_stats": get_stats
+ }
diff --git a/Co-creation-projects/aug618-Praxis/memory/storage/__init__.py b/Co-creation-projects/aug618-Praxis/memory/storage/__init__.py
new file mode 100644
index 00000000..2cbeb352
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/storage/__init__.py
@@ -0,0 +1,18 @@
+"""存储层模块
+
+按照第8章架构设计的存储层:
+- DocumentStore: 文档存储
+- QdrantVectorStore: Qdrant向量存储
+- Neo4jGraphStore: Neo4j图存储
+"""
+
+from .qdrant_store import QdrantVectorStore, QdrantConnectionManager
+from .neo4j_store import Neo4jGraphStore
+from .document_store import DocumentStore, SQLiteDocumentStore
+__all__ = [
+ "QdrantVectorStore",
+ "QdrantConnectionManager",
+ "Neo4jGraphStore",
+ "DocumentStore",
+ "SQLiteDocumentStore"
+]
diff --git a/Co-creation-projects/aug618-Praxis/memory/storage/document_store.py b/Co-creation-projects/aug618-Praxis/memory/storage/document_store.py
new file mode 100644
index 00000000..f7712023
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/storage/document_store.py
@@ -0,0 +1,462 @@
+"""文档存储实现
+
+支持多种文档数据库后端:
+- SQLite: 轻量级关系型数据库
+- PostgreSQL: 企业级关系型数据库(可扩展)
+"""
+
+from abc import ABC, abstractmethod
+from typing import List, Dict, Any, Optional
+import sqlite3
+import json
+import os
+import threading
+
+
+class DocumentStore(ABC):
+ """文档存储基类"""
+
+ @abstractmethod
+ def add_memory(
+ self,
+ memory_id: str,
+ user_id: str,
+ content: str,
+ memory_type: str,
+ timestamp: int,
+ importance: float,
+ properties: Dict[str, Any] = None
+ ) -> str:
+ """添加记忆"""
+ pass
+
+ @abstractmethod
+ def get_memory(self, memory_id: str) -> Optional[Dict[str, Any]]:
+ """获取单个记忆"""
+ pass
+
+ @abstractmethod
+ def search_memories(
+ self,
+ user_id: Optional[str] = None,
+ memory_type: Optional[str] = None,
+ text_query: Optional[str] = None,
+ start_time: Optional[int] = None,
+ end_time: Optional[int] = None,
+ importance_threshold: Optional[float] = None,
+ limit: int = 10
+ ) -> List[Dict[str, Any]]:
+ """搜索记忆"""
+ pass
+
+ @abstractmethod
+ def update_memory(
+ self,
+ memory_id: str,
+ content: str = None,
+ importance: float = None,
+ properties: Dict[str, Any] = None
+ ) -> bool:
+ """更新记忆"""
+ pass
+
+ @abstractmethod
+ def delete_memory(self, memory_id: str) -> bool:
+ """删除记忆"""
+ pass
+
+ @abstractmethod
+ def get_database_stats(self) -> Dict[str, Any]:
+ """获取数据库统计信息"""
+ pass
+
+ @abstractmethod
+ def add_document(self, content: str, metadata: Dict[str, Any] = None) -> str:
+ """添加文档"""
+ pass
+
+ @abstractmethod
+ def get_document(self, document_id: str) -> Optional[Dict[str, Any]]:
+ """获取文档"""
+ pass
+
+class SQLiteDocumentStore(DocumentStore):
+ """SQLite文档存储实现"""
+
+ _instances = {} # 存储已创建的实例
+ _initialized_dbs = set() # 存储已初始化的数据库路径
+
+ def __new__(cls, db_path: str = "./memory.db"):
+ """单例模式,同一路径只创建一个实例"""
+ abs_path = os.path.abspath(db_path)
+ if abs_path not in cls._instances:
+ instance = super(SQLiteDocumentStore, cls).__new__(cls)
+ cls._instances[abs_path] = instance
+ return cls._instances[abs_path]
+
+ def __init__(self, db_path: str = "./memory.db"):
+ # 避免重复初始化
+ if hasattr(self, '_initialized'):
+ return
+
+ self.db_path = db_path
+ self.local = threading.local()
+
+ # 确保目录存在
+ os.makedirs(os.path.dirname(os.path.abspath(db_path)), exist_ok=True)
+
+ # 初始化数据库(只初始化一次)
+ abs_path = os.path.abspath(db_path)
+ if abs_path not in self._initialized_dbs:
+ self._init_database()
+ self._initialized_dbs.add(abs_path)
+ print(f"[OK] SQLite 文档存储初始化完成: {db_path}")
+
+ self._initialized = True
+
+ def _get_connection(self):
+ """获取线程本地连接"""
+ if not hasattr(self.local, 'connection'):
+ self.local.connection = sqlite3.connect(self.db_path)
+ self.local.connection.row_factory = sqlite3.Row # 使结果可以按列名访问
+ return self.local.connection
+
+ def _init_database(self):
+ """初始化数据库表"""
+ conn = self._get_connection()
+ cursor = conn.cursor()
+
+ # 创建用户表
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS users (
+ id TEXT PRIMARY KEY,
+ name TEXT,
+ properties TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+
+ # 创建记忆表
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS memories (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ content TEXT NOT NULL,
+ memory_type TEXT NOT NULL,
+ timestamp INTEGER NOT NULL,
+ importance REAL NOT NULL,
+ properties TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users (id)
+ )
+ """)
+
+ # 创建概念表
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS concepts (
+ id TEXT PRIMARY KEY,
+ name TEXT NOT NULL,
+ description TEXT,
+ properties TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+
+ # 创建记忆-概念关联表
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS memory_concepts (
+ memory_id TEXT NOT NULL,
+ concept_id TEXT NOT NULL,
+ relevance_score REAL DEFAULT 1.0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (memory_id, concept_id),
+ FOREIGN KEY (memory_id) REFERENCES memories (id) ON DELETE CASCADE,
+ FOREIGN KEY (concept_id) REFERENCES concepts (id) ON DELETE CASCADE
+ )
+ """)
+
+ # 创建概念关系表
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS concept_relationships (
+ from_concept_id TEXT NOT NULL,
+ to_concept_id TEXT NOT NULL,
+ relationship_type TEXT NOT NULL,
+ strength REAL DEFAULT 1.0,
+ properties TEXT,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (from_concept_id, to_concept_id, relationship_type),
+ FOREIGN KEY (from_concept_id) REFERENCES concepts (id) ON DELETE CASCADE,
+ FOREIGN KEY (to_concept_id) REFERENCES concepts (id) ON DELETE CASCADE
+ )
+ """)
+
+ # 创建索引
+ indexes = [
+ "CREATE INDEX IF NOT EXISTS idx_memories_user_id ON memories (user_id)",
+ "CREATE INDEX IF NOT EXISTS idx_memories_type ON memories (memory_type)",
+ "CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON memories (timestamp)",
+ "CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories (importance)",
+ "CREATE INDEX IF NOT EXISTS idx_memory_concepts_memory ON memory_concepts (memory_id)",
+ "CREATE INDEX IF NOT EXISTS idx_memory_concepts_concept ON memory_concepts (concept_id)"
+ ]
+
+ for index_sql in indexes:
+ cursor.execute(index_sql)
+
+ conn.commit()
+ print("[OK] SQLite 数据库表和索引创建完成")
+
+ def add_memory(
+ self,
+ memory_id: str,
+ user_id: str,
+ content: str,
+ memory_type: str,
+ timestamp: int,
+ importance: float,
+ properties: Dict[str, Any] = None
+ ) -> str:
+ """添加记忆"""
+ conn = self._get_connection()
+ cursor = conn.cursor()
+
+ # 确保用户存在
+ cursor.execute("INSERT OR IGNORE INTO users (id, name) VALUES (?, ?)", (user_id, user_id))
+
+ # 插入记忆
+ cursor.execute("""
+ INSERT OR REPLACE INTO memories
+ (id, user_id, content, memory_type, timestamp, importance, properties, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
+ """, (
+ memory_id,
+ user_id,
+ content,
+ memory_type,
+ timestamp,
+ importance,
+ json.dumps(properties) if properties else None
+ ))
+
+ conn.commit()
+ return memory_id
+
+ def get_memory(self, memory_id: str) -> Optional[Dict[str, Any]]:
+ """获取单个记忆"""
+ conn = self._get_connection()
+ cursor = conn.cursor()
+
+ cursor.execute("""
+ SELECT id, user_id, content, memory_type, timestamp, importance, properties, created_at
+ FROM memories
+ WHERE id = ?
+ """, (memory_id,))
+
+ row = cursor.fetchone()
+ if not row:
+ return None
+
+ return {
+ "memory_id": row["id"],
+ "user_id": row["user_id"],
+ "content": row["content"],
+ "memory_type": row["memory_type"],
+ "timestamp": row["timestamp"],
+ "importance": row["importance"],
+ "properties": json.loads(row["properties"]) if row["properties"] else {},
+ "created_at": row["created_at"]
+ }
+
+ def search_memories(
+ self,
+ user_id: Optional[str] = None,
+ memory_type: Optional[str] = None,
+ text_query: Optional[str] = None,
+ start_time: Optional[int] = None,
+ end_time: Optional[int] = None,
+ importance_threshold: Optional[float] = None,
+ limit: int = 10
+ ) -> List[Dict[str, Any]]:
+ """搜索记忆"""
+ conn = self._get_connection()
+ cursor = conn.cursor()
+
+ # 构建查询条件
+ where_conditions = []
+ params = []
+
+ if user_id:
+ where_conditions.append("user_id = ?")
+ params.append(user_id)
+
+ if memory_type:
+ where_conditions.append("memory_type = ?")
+ params.append(memory_type)
+
+ if text_query:
+ where_conditions.append("content LIKE ?")
+ params.append(f"%{text_query}%")
+
+ if start_time:
+ where_conditions.append("timestamp >= ?")
+ params.append(start_time)
+
+ if end_time:
+ where_conditions.append("timestamp <= ?")
+ params.append(end_time)
+
+ if importance_threshold:
+ where_conditions.append("importance >= ?")
+ params.append(importance_threshold)
+
+ where_clause = ""
+ if where_conditions:
+ where_clause = "WHERE " + " AND ".join(where_conditions)
+
+ cursor.execute(f"""
+ SELECT id, user_id, content, memory_type, timestamp, importance, properties, created_at
+ FROM memories
+ {where_clause}
+ ORDER BY importance DESC, timestamp DESC
+ LIMIT ?
+ """, params + [limit])
+
+ memories = []
+ for row in cursor.fetchall():
+ memories.append({
+ "memory_id": row["id"],
+ "user_id": row["user_id"],
+ "content": row["content"],
+ "memory_type": row["memory_type"],
+ "timestamp": row["timestamp"],
+ "importance": row["importance"],
+ "properties": json.loads(row["properties"]) if row["properties"] else {},
+ "created_at": row["created_at"]
+ })
+
+ return memories
+
+ def update_memory(
+ self,
+ memory_id: str,
+ content: str = None,
+ importance: float = None,
+ properties: Dict[str, Any] = None
+ ) -> bool:
+ """更新记忆"""
+ conn = self._get_connection()
+ cursor = conn.cursor()
+
+ # 构建更新字段
+ update_fields = []
+ params = []
+
+ if content is not None:
+ update_fields.append("content = ?")
+ params.append(content)
+
+ if importance is not None:
+ update_fields.append("importance = ?")
+ params.append(importance)
+
+ if properties is not None:
+ update_fields.append("properties = ?")
+ params.append(json.dumps(properties))
+
+ if not update_fields:
+ return False
+
+ update_fields.append("updated_at = CURRENT_TIMESTAMP")
+ params.append(memory_id)
+
+ cursor.execute(f"""
+ UPDATE memories
+ SET {', '.join(update_fields)}
+ WHERE id = ?
+ """, params)
+
+ conn.commit()
+ return cursor.rowcount > 0
+
+ def delete_memory(self, memory_id: str) -> bool:
+ """删除记忆"""
+ conn = self._get_connection()
+ cursor = conn.cursor()
+
+ cursor.execute("DELETE FROM memories WHERE id = ?", (memory_id,))
+ deleted_count = cursor.rowcount
+
+ conn.commit()
+ return deleted_count > 0
+
+ def get_database_stats(self) -> Dict[str, Any]:
+ """获取数据库统计信息"""
+ conn = self._get_connection()
+ cursor = conn.cursor()
+
+ stats = {}
+
+ # 统计各表的记录数
+ tables = ["users", "memories", "concepts", "memory_concepts", "concept_relationships"]
+ for table in tables:
+ cursor.execute(f"SELECT COUNT(*) as count FROM {table}")
+ stats[f"{table}_count"] = cursor.fetchone()["count"]
+
+ # 统计记忆类型分布
+ cursor.execute("""
+ SELECT memory_type, COUNT(*) as count
+ FROM memories
+ GROUP BY memory_type
+ """)
+ memory_types = {}
+ for row in cursor.fetchall():
+ memory_types[row["memory_type"]] = row["count"]
+ stats["memory_types"] = memory_types
+
+ # 统计用户分布
+ cursor.execute("""
+ SELECT user_id, COUNT(*) as count
+ FROM memories
+ GROUP BY user_id
+ ORDER BY count DESC
+ LIMIT 10
+ """)
+ top_users = {}
+ for row in cursor.fetchall():
+ top_users[row["user_id"]] = row["count"]
+ stats["top_users"] = top_users
+
+ stats["store_type"] = "sqlite"
+ stats["db_path"] = self.db_path
+
+ return stats
+
+ def add_document(self, content: str, metadata: Dict[str, Any] = None) -> str:
+ """添加文档"""
+ import uuid
+ import time
+
+ doc_id = str(uuid.uuid4())
+ user_id = metadata.get("user_id", "system") if metadata else "system"
+
+ return self.add_memory(
+ memory_id=doc_id,
+ user_id=user_id,
+ content=content,
+ memory_type="document",
+ timestamp=int(time.time()),
+ importance=0.5,
+ properties=metadata or {}
+ )
+
+ def get_document(self, document_id: str) -> Optional[Dict[str, Any]]:
+ """获取文档"""
+ return self.get_memory(document_id)
+
+ def close(self):
+ """关闭数据库连接"""
+ if hasattr(self.local, 'connection'):
+ self.local.connection.close()
+ delattr(self.local, 'connection')
+ print("[OK] SQLite 连接已关闭")
diff --git a/Co-creation-projects/aug618-Praxis/memory/storage/neo4j_store.py b/Co-creation-projects/aug618-Praxis/memory/storage/neo4j_store.py
new file mode 100644
index 00000000..969b3198
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/storage/neo4j_store.py
@@ -0,0 +1,455 @@
+"""
+Neo4j图数据库存储实现
+"""
+
+import logging
+from typing import Dict, List, Optional, Any, Tuple
+from datetime import datetime
+
+try:
+ from neo4j import GraphDatabase
+ from neo4j.exceptions import ServiceUnavailable, AuthError
+ NEO4J_AVAILABLE = True
+except ImportError:
+ NEO4J_AVAILABLE = False
+ GraphDatabase = None
+
+logger = logging.getLogger(__name__)
+
+class Neo4jGraphStore:
+ """Neo4j图数据库存储实现"""
+
+ def __init__(
+ self,
+ uri: str = "bolt://localhost:7687",
+ username: str = "neo4j",
+ password: str = "hello-agents-password",
+ database: str = "neo4j",
+ max_connection_lifetime: int = 3600,
+ max_connection_pool_size: int = 50,
+ connection_acquisition_timeout: int = 60,
+ **kwargs
+ ):
+ """
+ 初始化Neo4j图存储 (支持云API)
+
+ Args:
+ uri: Neo4j连接URI (本地: bolt://localhost:7687, 云: neo4j+s://xxx.databases.neo4j.io)
+ username: 用户名
+ password: 密码
+ database: 数据库名称
+ max_connection_lifetime: 最大连接生命周期(秒)
+ max_connection_pool_size: 最大连接池大小
+ connection_acquisition_timeout: 连接获取超时(秒)
+ """
+ if not NEO4J_AVAILABLE:
+ raise ImportError(
+ "neo4j未安装。请运行: pip install neo4j>=5.0.0"
+ )
+
+ self.uri = uri
+ self.username = username
+ self.password = password
+ self.database = database
+
+ # 初始化驱动
+ self.driver = None
+ self._initialize_driver(
+ max_connection_lifetime=max_connection_lifetime,
+ max_connection_pool_size=max_connection_pool_size,
+ connection_acquisition_timeout=connection_acquisition_timeout
+ )
+
+ # 创建索引
+ self._create_indexes()
+
+ def _initialize_driver(self, **config):
+ """初始化Neo4j驱动"""
+ try:
+ self.driver = GraphDatabase.driver(
+ self.uri,
+ auth=(self.username, self.password),
+ **config
+ )
+
+ # 验证连接
+ self.driver.verify_connectivity()
+
+ # 检查是否是云服务
+ if "neo4j.io" in self.uri or "aura" in self.uri.lower():
+ logger.info(f"✅ 成功连接到Neo4j云服务: {self.uri}")
+ else:
+ logger.info(f"✅ 成功连接到Neo4j服务: {self.uri}")
+
+ except AuthError as e:
+ logger.error(f"❌ Neo4j认证失败: {e}")
+ logger.info("💡 请检查用户名和密码是否正确")
+ raise
+ except ServiceUnavailable as e:
+ logger.error(f"❌ Neo4j服务不可用: {e}")
+ if "localhost" in self.uri:
+ logger.info("💡 本地连接失败,可以考虑使用Neo4j Aura云服务")
+ logger.info("💡 或启动本地服务: docker run -p 7474:7474 -p 7687:7687 neo4j:5.14")
+ else:
+ logger.info("💡 请检查URL和网络连接")
+ raise
+ except Exception as e:
+ logger.error(f"❌ Neo4j连接失败: {e}")
+ raise
+
+ def _create_indexes(self):
+ """创建必要的索引以提高查询性能"""
+ indexes = [
+ # 实体索引
+ "CREATE INDEX entity_id_index IF NOT EXISTS FOR (e:Entity) ON (e.id)",
+ "CREATE INDEX entity_name_index IF NOT EXISTS FOR (e:Entity) ON (e.name)",
+ "CREATE INDEX entity_type_index IF NOT EXISTS FOR (e:Entity) ON (e.type)",
+
+ # 记忆索引
+ "CREATE INDEX memory_id_index IF NOT EXISTS FOR (m:Memory) ON (m.id)",
+ "CREATE INDEX memory_type_index IF NOT EXISTS FOR (m:Memory) ON (m.memory_type)",
+ "CREATE INDEX memory_timestamp_index IF NOT EXISTS FOR (m:Memory) ON (m.timestamp)",
+ ]
+
+ with self.driver.session(database=self.database) as session:
+ for index_query in indexes:
+ try:
+ session.run(index_query)
+ except Exception as e:
+ logger.debug(f"索引创建跳过 (可能已存在): {e}")
+
+ logger.info("✅ Neo4j索引创建完成")
+
+ def add_entity(self, entity_id: str, name: str, entity_type: str, properties: Dict[str, Any] = None) -> bool:
+ """
+ 添加实体节点
+
+ Args:
+ entity_id: 实体ID
+ name: 实体名称
+ entity_type: 实体类型
+ properties: 附加属性
+
+ Returns:
+ bool: 是否成功
+ """
+ try:
+ props = properties or {}
+ props.update({
+ "id": entity_id,
+ "name": name,
+ "type": entity_type,
+ "created_at": datetime.now().isoformat(),
+ "updated_at": datetime.now().isoformat()
+ })
+
+ query = """
+ MERGE (e:Entity {id: $entity_id})
+ SET e += $properties
+ RETURN e
+ """
+
+ with self.driver.session(database=self.database) as session:
+ result = session.run(query, entity_id=entity_id, properties=props)
+ record = result.single()
+
+ if record:
+ logger.debug(f"✅ 添加实体: {name} ({entity_type})")
+ return True
+ return False
+
+ except Exception as e:
+ logger.error(f"❌ 添加实体失败: {e}")
+ return False
+
+ def add_relationship(
+ self,
+ from_entity_id: str,
+ to_entity_id: str,
+ relationship_type: str,
+ properties: Dict[str, Any] = None
+ ) -> bool:
+ """
+ 添加实体间关系
+
+ Args:
+ from_entity_id: 源实体ID
+ to_entity_id: 目标实体ID
+ relationship_type: 关系类型
+ properties: 关系属性
+
+ Returns:
+ bool: 是否成功
+ """
+ try:
+ props = properties or {}
+ props.update({
+ "type": relationship_type,
+ "created_at": datetime.now().isoformat(),
+ "updated_at": datetime.now().isoformat()
+ })
+
+ query = f"""
+ MATCH (from:Entity {{id: $from_id}})
+ MATCH (to:Entity {{id: $to_id}})
+ MERGE (from)-[r:{relationship_type}]->(to)
+ SET r += $properties
+ RETURN r
+ """
+
+ with self.driver.session(database=self.database) as session:
+ result = session.run(
+ query,
+ from_id=from_entity_id,
+ to_id=to_entity_id,
+ properties=props
+ )
+ record = result.single()
+
+ if record:
+ logger.debug(f"✅ 添加关系: {from_entity_id} -{relationship_type}-> {to_entity_id}")
+ return True
+ return False
+
+ except Exception as e:
+ logger.error(f"❌ 添加关系失败: {e}")
+ return False
+
+ def find_related_entities(
+ self,
+ entity_id: str,
+ relationship_types: List[str] = None,
+ max_depth: int = 2,
+ limit: int = 50
+ ) -> List[Dict[str, Any]]:
+ """
+ 查找相关实体
+
+ Args:
+ entity_id: 起始实体ID
+ relationship_types: 关系类型过滤
+ max_depth: 最大搜索深度
+ limit: 结果限制
+
+ Returns:
+ List[Dict]: 相关实体列表
+ """
+ try:
+ # 构建关系类型过滤
+ rel_filter = ""
+ if relationship_types:
+ rel_types = "|".join(relationship_types)
+ rel_filter = f":{rel_types}"
+
+ query = f"""
+ MATCH path = (start:Entity {{id: $entity_id}})-[r{rel_filter}*1..{max_depth}]-(related:Entity)
+ WHERE start.id <> related.id
+ RETURN DISTINCT related,
+ length(path) as distance,
+ [rel in relationships(path) | type(rel)] as relationship_path
+ ORDER BY distance, related.name
+ LIMIT $limit
+ """
+
+ with self.driver.session(database=self.database) as session:
+ result = session.run(query, entity_id=entity_id, limit=limit)
+
+ entities = []
+ for record in result:
+ entity_data = dict(record["related"])
+ entity_data["distance"] = record["distance"]
+ entity_data["relationship_path"] = record["relationship_path"]
+ entities.append(entity_data)
+
+ logger.debug(f"🔍 找到 {len(entities)} 个相关实体")
+ return entities
+
+ except Exception as e:
+ logger.error(f"❌ 查找相关实体失败: {e}")
+ return []
+
+ def search_entities_by_name(self, name_pattern: str, entity_types: List[str] = None, limit: int = 20) -> List[Dict[str, Any]]:
+ """
+ 按名称搜索实体
+
+ Args:
+ name_pattern: 名称模式 (支持部分匹配)
+ entity_types: 实体类型过滤
+ limit: 结果限制
+
+ Returns:
+ List[Dict]: 匹配的实体列表
+ """
+ try:
+ # 构建类型过滤
+ type_filter = ""
+ params = {"pattern": f".*{name_pattern}.*", "limit": limit}
+
+ if entity_types:
+ type_filter = "AND e.type IN $types"
+ params["types"] = entity_types
+
+ query = f"""
+ MATCH (e:Entity)
+ WHERE e.name =~ $pattern {type_filter}
+ RETURN e
+ ORDER BY e.name
+ LIMIT $limit
+ """
+
+ with self.driver.session(database=self.database) as session:
+ result = session.run(query, **params)
+
+ entities = []
+ for record in result:
+ entity_data = dict(record["e"])
+ entities.append(entity_data)
+
+ logger.debug(f"🔍 按名称搜索到 {len(entities)} 个实体")
+ return entities
+
+ except Exception as e:
+ logger.error(f"❌ 按名称搜索实体失败: {e}")
+ return []
+
+ def get_entity_relationships(self, entity_id: str) -> List[Dict[str, Any]]:
+ """
+ 获取实体的所有关系
+
+ Args:
+ entity_id: 实体ID
+
+ Returns:
+ List[Dict]: 关系列表
+ """
+ try:
+ query = """
+ MATCH (e:Entity {id: $entity_id})-[r]-(other:Entity)
+ RETURN r, other,
+ CASE WHEN startNode(r).id = $entity_id THEN 'outgoing' ELSE 'incoming' END as direction
+ """
+
+ with self.driver.session(database=self.database) as session:
+ result = session.run(query, entity_id=entity_id)
+
+ relationships = []
+ for record in result:
+ rel_data = dict(record["r"])
+ other_data = dict(record["other"])
+
+ relationship = {
+ "relationship": rel_data,
+ "other_entity": other_data,
+ "direction": record["direction"]
+ }
+ relationships.append(relationship)
+
+ return relationships
+
+ except Exception as e:
+ logger.error(f"❌ 获取实体关系失败: {e}")
+ return []
+
+ def delete_entity(self, entity_id: str) -> bool:
+ """
+ 删除实体及其所有关系
+
+ Args:
+ entity_id: 实体ID
+
+ Returns:
+ bool: 是否成功
+ """
+ try:
+ query = """
+ MATCH (e:Entity {id: $entity_id})
+ DETACH DELETE e
+ """
+
+ with self.driver.session(database=self.database) as session:
+ result = session.run(query, entity_id=entity_id)
+ summary = result.consume()
+
+ deleted_count = summary.counters.nodes_deleted
+ logger.info(f"✅ 删除实体: {entity_id} (删除 {deleted_count} 个节点)")
+ return deleted_count > 0
+
+ except Exception as e:
+ logger.error(f"❌ 删除实体失败: {e}")
+ return False
+
+ def clear_all(self) -> bool:
+ """
+ 清空所有数据
+
+ Returns:
+ bool: 是否成功
+ """
+ try:
+ query = "MATCH (n) DETACH DELETE n"
+
+ with self.driver.session(database=self.database) as session:
+ result = session.run(query)
+ summary = result.consume()
+
+ deleted_nodes = summary.counters.nodes_deleted
+ deleted_relationships = summary.counters.relationships_deleted
+
+ logger.info(f"✅ 清空Neo4j数据库: 删除 {deleted_nodes} 个节点, {deleted_relationships} 个关系")
+ return True
+
+ except Exception as e:
+ logger.error(f"❌ 清空数据库失败: {e}")
+ return False
+
+ def get_stats(self) -> Dict[str, Any]:
+ """
+ 获取图数据库统计信息
+
+ Returns:
+ Dict: 统计信息
+ """
+ try:
+ queries = {
+ "total_nodes": "MATCH (n) RETURN count(n) as count",
+ "total_relationships": "MATCH ()-[r]->() RETURN count(r) as count",
+ "entity_nodes": "MATCH (n:Entity) RETURN count(n) as count",
+ "memory_nodes": "MATCH (n:Memory) RETURN count(n) as count",
+ }
+
+ stats = {}
+ with self.driver.session(database=self.database) as session:
+ for key, query in queries.items():
+ result = session.run(query)
+ record = result.single()
+ stats[key] = record["count"] if record else 0
+
+ return stats
+
+ except Exception as e:
+ logger.error(f"❌ 获取统计信息失败: {e}")
+ return {}
+
+ def health_check(self) -> bool:
+ """
+ 健康检查
+
+ Returns:
+ bool: 服务是否健康
+ """
+ try:
+ with self.driver.session(database=self.database) as session:
+ result = session.run("RETURN 1 as health")
+ record = result.single()
+ return record["health"] == 1
+ except Exception as e:
+ logger.error(f"❌ Neo4j健康检查失败: {e}")
+ return False
+
+ def __del__(self):
+ """析构函数,清理资源"""
+ if hasattr(self, 'driver') and self.driver:
+ try:
+ self.driver.close()
+ except:
+ pass
diff --git a/Co-creation-projects/aug618-Praxis/memory/storage/qdrant_store.py b/Co-creation-projects/aug618-Praxis/memory/storage/qdrant_store.py
new file mode 100644
index 00000000..16a1f9da
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/storage/qdrant_store.py
@@ -0,0 +1,542 @@
+"""
+Qdrant向量数据库存储实现
+使用专业的Qdrant向量数据库替代ChromaDB
+"""
+
+import logging
+import os
+import uuid
+import threading
+from typing import Dict, List, Optional, Any, Union
+import numpy as np
+from datetime import datetime
+
+try:
+ from qdrant_client import QdrantClient
+ from qdrant_client.http import models
+ from qdrant_client.http.models import (
+ Distance, VectorParams, PointStruct,
+ Filter, FieldCondition, MatchValue, SearchRequest
+ )
+ QDRANT_AVAILABLE = True
+except ImportError:
+ QDRANT_AVAILABLE = False
+ QdrantClient = None
+ models = None
+
+logger = logging.getLogger(__name__)
+
+class QdrantConnectionManager:
+ """Qdrant连接管理器 - 防止重复连接和初始化"""
+ _instances = {} # key: (url, collection_name) -> QdrantVectorStore instance
+ _lock = threading.Lock()
+
+ @classmethod
+ def get_instance(
+ cls,
+ url: Optional[str] = None,
+ api_key: Optional[str] = None,
+ collection_name: str = "hello_agents_vectors",
+ vector_size: int = 384,
+ distance: str = "cosine",
+ timeout: int = 30,
+ **kwargs
+ ) -> 'QdrantVectorStore':
+ """获取或创建Qdrant实例(单例模式)"""
+ # 创建唯一键
+ key = (url or "local", collection_name)
+
+ if key not in cls._instances:
+ with cls._lock:
+ # 双重检查锁定
+ if key not in cls._instances:
+ logger.debug(f"🔄 创建新的Qdrant连接: {collection_name}")
+ cls._instances[key] = QdrantVectorStore(
+ url=url,
+ api_key=api_key,
+ collection_name=collection_name,
+ vector_size=vector_size,
+ distance=distance,
+ timeout=timeout,
+ **kwargs
+ )
+ else:
+ logger.debug(f"♻️ 复用现有Qdrant连接: {collection_name}")
+ else:
+ logger.debug(f"♻️ 复用现有Qdrant连接: {collection_name}")
+
+ return cls._instances[key]
+
+class QdrantVectorStore:
+ """Qdrant向量数据库存储实现"""
+
+ def __init__(
+ self,
+ url: Optional[str] = None,
+ api_key: Optional[str] = None,
+ collection_name: str = "hello_agents_vectors",
+ vector_size: int = 384,
+ distance: str = "cosine",
+ timeout: int = 30,
+ **kwargs
+ ):
+ """
+ 初始化Qdrant向量存储 (支持云API)
+
+ Args:
+ url: Qdrant云服务URL (如果为None则使用本地)
+ api_key: Qdrant云服务API密钥
+ collection_name: 集合名称
+ vector_size: 向量维度
+ distance: 距离度量方式 (cosine, dot, euclidean)
+ timeout: 连接超时时间
+ """
+ if not QDRANT_AVAILABLE:
+ raise ImportError(
+ "qdrant-client未安装。请运行: pip install qdrant-client>=1.6.0"
+ )
+
+ self.url = url
+ self.api_key = api_key
+ self.collection_name = collection_name
+ self.vector_size = vector_size
+ self.timeout = timeout
+ # HNSW/Query params via env
+ try:
+ self.hnsw_m = int(os.getenv("QDRANT_HNSW_M", "32"))
+ except Exception:
+ self.hnsw_m = 32
+ try:
+ self.hnsw_ef_construct = int(os.getenv("QDRANT_HNSW_EF_CONSTRUCT", "256"))
+ except Exception:
+ self.hnsw_ef_construct = 256
+ try:
+ self.search_ef = int(os.getenv("QDRANT_SEARCH_EF", "128"))
+ except Exception:
+ self.search_ef = 128
+ self.search_exact = os.getenv("QDRANT_SEARCH_EXACT", "0") == "1"
+
+ # 距离度量映射
+ distance_map = {
+ "cosine": Distance.COSINE,
+ "dot": Distance.DOT,
+ "euclidean": Distance.EUCLID,
+ }
+ self.distance = distance_map.get(distance.lower(), Distance.COSINE)
+
+ # 初始化客户端
+ self.client = None
+ self._initialize_client()
+
+ def _initialize_client(self):
+ """初始化Qdrant客户端和集合"""
+ try:
+ # 根据配置创建客户端连接
+ if self.url and self.api_key:
+ # 使用云服务API
+ self.client = QdrantClient(
+ url=self.url,
+ api_key=self.api_key,
+ timeout=self.timeout
+ )
+ logger.info(f"✅ 成功连接到Qdrant云服务: {self.url}")
+ elif self.url:
+ # 使用自定义URL(无API密钥)
+ self.client = QdrantClient(
+ url=self.url,
+ timeout=self.timeout
+ )
+ logger.info(f"✅ 成功连接到Qdrant服务: {self.url}")
+ else:
+ # 使用本地服务(默认)
+ self.client = QdrantClient(
+ host="localhost",
+ port=6333,
+ timeout=self.timeout
+ )
+ logger.info("✅ 成功连接到本地Qdrant服务: localhost:6333")
+
+ # 检查连接
+ collections = self.client.get_collections()
+
+ # 创建或获取集合
+ self._ensure_collection()
+
+ except Exception as e:
+ logger.error(f"❌ Qdrant连接失败: {e}")
+ if not self.url:
+ logger.info("💡 本地连接失败,可以考虑使用Qdrant云服务")
+ logger.info("💡 或启动本地服务: docker run -p 6333:6333 qdrant/qdrant")
+ else:
+ logger.info("💡 请检查URL和API密钥是否正确")
+ raise
+
+ def _ensure_collection(self):
+ """确保集合存在,不存在则创建"""
+ try:
+ # 检查集合是否存在
+ collections = self.client.get_collections().collections
+ collection_names = [c.name for c in collections]
+
+ if self.collection_name not in collection_names:
+ # 创建新集合
+ hnsw_cfg = None
+ try:
+ hnsw_cfg = models.HnswConfigDiff(m=self.hnsw_m, ef_construct=self.hnsw_ef_construct)
+ except Exception:
+ hnsw_cfg = None
+ self.client.create_collection(
+ collection_name=self.collection_name,
+ vectors_config=VectorParams(
+ size=self.vector_size,
+ distance=self.distance
+ ),
+ hnsw_config=hnsw_cfg
+ )
+ logger.info(f"✅ 创建Qdrant集合: {self.collection_name}")
+ else:
+ logger.info(f"✅ 使用现有Qdrant集合: {self.collection_name}")
+ # 尝试更新 HNSW 配置
+ try:
+ self.client.update_collection(
+ collection_name=self.collection_name,
+ hnsw_config=models.HnswConfigDiff(m=self.hnsw_m, ef_construct=self.hnsw_ef_construct)
+ )
+ except Exception as ie:
+ logger.debug(f"跳过更新HNSW配置: {ie}")
+ # 确保必要的payload索引
+ self._ensure_payload_indexes()
+
+ except Exception as e:
+ logger.error(f"❌ 集合初始化失败: {e}")
+ raise
+
+ def _ensure_payload_indexes(self):
+ """为常用过滤字段创建payload索引"""
+ try:
+ index_fields = [
+ ("memory_type", models.PayloadSchemaType.KEYWORD),
+ ("user_id", models.PayloadSchemaType.KEYWORD),
+ ("memory_id", models.PayloadSchemaType.KEYWORD),
+ ("timestamp", models.PayloadSchemaType.INTEGER),
+ ("modality", models.PayloadSchemaType.KEYWORD), # 感知记忆模态筛选
+ ("source", models.PayloadSchemaType.KEYWORD),
+ ("external", models.PayloadSchemaType.BOOL),
+ ("namespace", models.PayloadSchemaType.KEYWORD),
+ # RAG相关字段索引
+ ("is_rag_data", models.PayloadSchemaType.BOOL),
+ ("rag_namespace", models.PayloadSchemaType.KEYWORD),
+ ("data_source", models.PayloadSchemaType.KEYWORD),
+ ]
+ for field_name, schema_type in index_fields:
+ try:
+ self.client.create_payload_index(
+ collection_name=self.collection_name,
+ field_name=field_name,
+ field_schema=schema_type,
+ )
+ except Exception as ie:
+ # 索引已存在会报错,忽略
+ logger.debug(f"索引 {field_name} 已存在或创建失败: {ie}")
+ except Exception as e:
+ logger.debug(f"创建payload索引时出错: {e}")
+
+ def add_vectors(
+ self,
+ vectors: List[List[float]],
+ metadata: List[Dict[str, Any]],
+ ids: Optional[List[str]] = None
+ ) -> bool:
+ """
+ 添加向量到Qdrant
+
+ Args:
+ vectors: 向量列表
+ metadata: 元数据列表
+ ids: 可选的ID列表
+
+ Returns:
+ bool: 是否成功
+ """
+ try:
+ if not vectors:
+ logger.warning("⚠️ 向量列表为空")
+ return False
+
+ # 生成ID(如果未提供)
+ if ids is None:
+ ids = [f"vec_{i}_{int(datetime.now().timestamp() * 1000000)}"
+ for i in range(len(vectors))]
+
+ # 构建点数据
+ logger.info(f"[Qdrant] add_vectors start: n_vectors={len(vectors)} n_meta={len(metadata)} collection={self.collection_name}")
+ points = []
+ for i, (vector, meta, point_id) in enumerate(zip(vectors, metadata, ids)):
+ # 确保向量是正确的维度
+ try:
+ vlen = len(vector)
+ except Exception:
+ logger.error(f"[Qdrant] 非法向量类型: index={i} type={type(vector)} value={vector}")
+ continue
+ if vlen != self.vector_size:
+ logger.warning(f"⚠️ 向量维度不匹配: 期望{self.vector_size}, 实际{len(vector)}")
+ continue
+
+ # 添加时间戳到元数据
+ meta_with_timestamp = meta.copy()
+ meta_with_timestamp["timestamp"] = int(datetime.now().timestamp())
+ meta_with_timestamp["added_at"] = int(datetime.now().timestamp())
+ if "external" in meta_with_timestamp and not isinstance(meta_with_timestamp.get("external"), bool):
+ # normalize to bool
+ val = meta_with_timestamp.get("external")
+ meta_with_timestamp["external"] = True if str(val).lower() in ("1", "true", "yes") else False
+ # 确保点ID是Qdrant接受的类型(无符号整数或UUID字符串)
+ safe_id: Any
+ if isinstance(point_id, int):
+ safe_id = point_id
+ elif isinstance(point_id, str):
+ try:
+ uuid.UUID(point_id)
+ safe_id = point_id
+ except Exception:
+ safe_id = str(uuid.uuid4())
+ else:
+ safe_id = str(uuid.uuid4())
+
+ point = PointStruct(
+ id=safe_id,
+ vector=vector,
+ payload=meta_with_timestamp
+ )
+ points.append(point)
+
+ if not points:
+ logger.warning("⚠️ 没有有效的向量点")
+ return False
+
+ # 批量插入
+ logger.info(f"[Qdrant] upsert begin: points={len(points)}")
+ operation_info = self.client.upsert(
+ collection_name=self.collection_name,
+ points=points,
+ wait=True
+ )
+ logger.info("[Qdrant] upsert done")
+
+ logger.info(f"✅ 成功添加 {len(points)} 个向量到Qdrant")
+ return True
+
+ except Exception as e:
+ logger.error(f"❌ 添加向量失败: {e}")
+ return False
+
+ def search_similar(
+ self,
+ query_vector: List[float],
+ limit: int = 10,
+ score_threshold: Optional[float] = None,
+ where: Optional[Dict[str, Any]] = None
+ ) -> List[Dict[str, Any]]:
+ """
+ 搜索相似向量
+
+ Args:
+ query_vector: 查询向量
+ limit: 返回结果数量限制
+ score_threshold: 相似度阈值
+ where: 过滤条件
+
+ Returns:
+ List[Dict]: 搜索结果
+ """
+ try:
+ if len(query_vector) != self.vector_size:
+ logger.error(f"❌ 查询向量维度错误: 期望{self.vector_size}, 实际{len(query_vector)}")
+ return []
+
+ # 构建过滤器
+ query_filter = None
+ if where:
+ conditions = []
+ for key, value in where.items():
+ if isinstance(value, (str, int, float, bool)):
+ conditions.append(
+ FieldCondition(
+ key=key,
+ match=MatchValue(value=value)
+ )
+ )
+
+ if conditions:
+ query_filter = Filter(must=conditions)
+
+ # 执行搜索
+ # 搜索参数
+ search_params = None
+ try:
+ search_params = models.SearchParams(hnsw_ef=self.search_ef, exact=self.search_exact)
+ except Exception:
+ search_params = None
+ response = self.client.query_points(
+ collection_name=self.collection_name,
+ query=query_vector,
+ query_filter=query_filter,
+ limit=limit,
+ score_threshold=score_threshold,
+ with_payload=True,
+ with_vectors=False,
+ search_params=search_params
+ )
+ search_result = response.points
+
+ # 转换结果格式
+ results = []
+ for hit in search_result:
+ result = {
+ "id": hit.id,
+ "score": hit.score,
+ "metadata": hit.payload or {}
+ }
+ results.append(result)
+
+ logger.debug(f"🔍 Qdrant搜索返回 {len(results)} 个结果")
+ return results
+
+ except Exception as e:
+ logger.error(f"❌ 向量搜索失败: {e}")
+ return []
+
+ def delete_vectors(self, ids: List[str]) -> bool:
+ """
+ 删除向量
+
+ Args:
+ ids: 要删除的向量ID列表
+
+ Returns:
+ bool: 是否成功
+ """
+ try:
+ if not ids:
+ return True
+
+ operation_info = self.client.delete(
+ collection_name=self.collection_name,
+ points_selector=models.PointIdsList(
+ points=ids
+ ),
+ wait=True
+ )
+
+ logger.info(f"✅ 成功删除 {len(ids)} 个向量")
+ return True
+
+ except Exception as e:
+ logger.error(f"❌ 删除向量失败: {e}")
+ return False
+
+ def clear_collection(self) -> bool:
+ """
+ 清空集合
+
+ Returns:
+ bool: 是否成功
+ """
+ try:
+ # 删除并重新创建集合
+ self.client.delete_collection(collection_name=self.collection_name)
+ self._ensure_collection()
+
+ logger.info(f"✅ 成功清空Qdrant集合: {self.collection_name}")
+ return True
+
+ except Exception as e:
+ logger.error(f"❌ 清空集合失败: {e}")
+ return False
+
+ def delete_memories(self, memory_ids: List[str]):
+ """
+ 删除指定记忆(通过payload中的 memory_id 过滤删除)
+
+ 注意:由于写入时可能将非UUID的点ID转换为UUID,这里不再依赖点ID,
+ 而是通过payload中的memory_id来匹配删除,确保一致性。
+ """
+ try:
+ if not memory_ids:
+ return
+ # 构建 should 过滤条件:memory_id 等于任一给定值
+ conditions = [
+ FieldCondition(key="memory_id", match=MatchValue(value=mid))
+ for mid in memory_ids
+ ]
+ query_filter = Filter(should=conditions)
+ self.client.delete(
+ collection_name=self.collection_name,
+ points_selector=models.FilterSelector(filter=query_filter),
+ wait=True,
+ )
+ logger.info(f"✅ 成功按memory_id删除 {len(memory_ids)} 个Qdrant向量")
+ except Exception as e:
+ logger.error(f"❌ 删除记忆失败: {e}")
+ raise
+
+ def get_collection_info(self) -> Dict[str, Any]:
+ """
+ 获取集合信息
+
+ Returns:
+ Dict: 集合信息
+ """
+ try:
+ collection_info = self.client.get_collection(self.collection_name)
+
+ info = {
+ "name": self.collection_name,
+ "vectors_count": collection_info.vectors_count,
+ "indexed_vectors_count": collection_info.indexed_vectors_count,
+ "points_count": collection_info.points_count,
+ "segments_count": collection_info.segments_count,
+ "config": {
+ "vector_size": self.vector_size,
+ "distance": self.distance.value,
+ }
+ }
+
+ return info
+
+ except Exception as e:
+ logger.error(f"❌ 获取集合信息失败: {e}")
+ return {}
+
+ def get_collection_stats(self) -> Dict[str, Any]:
+ """
+ 获取集合统计信息(兼容抽象接口)
+ """
+ info = self.get_collection_info()
+ if not info:
+ return {"store_type": "qdrant", "name": self.collection_name}
+ info["store_type"] = "qdrant"
+ return info
+
+ def health_check(self) -> bool:
+ """
+ 健康检查
+
+ Returns:
+ bool: 服务是否健康
+ """
+ try:
+ # 尝试获取集合列表
+ collections = self.client.get_collections()
+ return True
+ except Exception as e:
+ logger.error(f"❌ Qdrant健康检查失败: {e}")
+ return False
+
+ def __del__(self):
+ """析构函数,清理资源"""
+ if hasattr(self, 'client') and self.client:
+ try:
+ self.client.close()
+ except:
+ pass
diff --git a/Co-creation-projects/aug618-Praxis/memory/types/__init__.py b/Co-creation-projects/aug618-Praxis/memory/types/__init__.py
new file mode 100644
index 00000000..c91478ee
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/types/__init__.py
@@ -0,0 +1,27 @@
+"""记忆类型层模块
+
+按照第8章架构设计的记忆类型层:
+- WorkingMemory: 工作记忆 - 短期上下文管理
+- EpisodicMemory: 情景记忆 - 具体交互事件存储
+- SemanticMemory: 语义记忆 - 抽象知识和概念存储
+- PerceptualMemory: 感知记忆 - 多模态数据存储
+"""
+
+from .working import WorkingMemory
+from .episodic import EpisodicMemory, Episode
+from .semantic import SemanticMemory, Entity, Relation
+from .perceptual import PerceptualMemory, Perception
+
+__all__ = [
+ # 记忆类型
+ "WorkingMemory",
+ "EpisodicMemory",
+ "SemanticMemory",
+ "PerceptualMemory",
+
+ # 辅助类
+ "Episode",
+ "Entity",
+ "Relation",
+ "Perception"
+]
diff --git a/Co-creation-projects/aug618-Praxis/memory/types/episodic.py b/Co-creation-projects/aug618-Praxis/memory/types/episodic.py
new file mode 100644
index 00000000..f8f58f8c
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/types/episodic.py
@@ -0,0 +1,661 @@
+"""情景记忆实现
+
+按照第8章架构设计的情景记忆,提供:
+- 具体交互事件存储
+- 时间序列组织
+- 上下文丰富的记忆
+- 模式识别能力
+"""
+
+from typing import List, Dict, Any, Optional, Tuple
+from datetime import datetime, timedelta
+import os
+import math
+import json
+import logging
+
+logger = logging.getLogger(__name__)
+
+from ..base import BaseMemory, MemoryItem, MemoryConfig
+from ..storage import SQLiteDocumentStore, QdrantVectorStore
+from ..embedding import get_text_embedder, get_dimension
+
+class Episode:
+ """情景记忆中的单个情景"""
+
+ def __init__(
+ self,
+ episode_id: str,
+ user_id: str,
+ session_id: str,
+ timestamp: datetime,
+ content: str,
+ context: Dict[str, Any],
+ outcome: Optional[str] = None,
+ importance: float = 0.5
+ ):
+ self.episode_id = episode_id
+ self.user_id = user_id
+ self.session_id = session_id
+ self.timestamp = timestamp
+ self.content = content
+ self.context = context
+ self.outcome = outcome
+ self.importance = importance
+
+class EpisodicMemory(BaseMemory):
+ """情景记忆实现
+
+ 特点:
+ - 存储具体的交互事件
+ - 包含丰富的上下文信息
+ - 按时间序列组织
+ - 支持模式识别和回溯
+ """
+
+ def __init__(self, config: MemoryConfig, storage_backend=None):
+ super().__init__(config, storage_backend)
+
+ # 本地缓存(内存)
+ self.episodes: List[Episode] = []
+ self.sessions: Dict[str, List[str]] = {} # session_id -> episode_ids
+
+ # 模式识别缓存
+ self.patterns_cache = {}
+ self.last_pattern_analysis = None
+
+ # 权威文档存储(SQLite)
+ db_dir = self.config.storage_path if hasattr(self.config, 'storage_path') else "./memory_data"
+ os.makedirs(db_dir, exist_ok=True)
+ db_path = os.path.join(db_dir, "memory.db")
+ self.doc_store = SQLiteDocumentStore(db_path=db_path)
+
+ self.embedder = None
+ self.vector_store = None
+
+ disable_vec = bool(getattr(self.config, "disable_vector_store", False))
+ disable_emb = bool(getattr(self.config, "disable_embeddings", False))
+
+ # 统一嵌入模型 / 向量存储(可关闭:只用 SQLite + 文本检索)
+ if not disable_emb:
+ try:
+ self.embedder = get_text_embedder()
+ except Exception:
+ self.embedder = None
+
+ if not disable_vec and self.embedder is not None:
+ try:
+ from ..storage.qdrant_store import QdrantConnectionManager
+
+ qdrant_url = os.getenv("QDRANT_URL")
+ qdrant_api_key = os.getenv("QDRANT_API_KEY")
+ self.vector_store = QdrantConnectionManager.get_instance(
+ url=qdrant_url,
+ api_key=qdrant_api_key,
+ collection_name=os.getenv("QDRANT_COLLECTION", "hello_agents_vectors"),
+ vector_size=get_dimension(getattr(self.embedder, "dimension", 384)),
+ distance=os.getenv("QDRANT_DISTANCE", "cosine"),
+ )
+ except Exception:
+ self.vector_store = None
+
+ def add(self, memory_item: MemoryItem) -> str:
+ """添加情景记忆"""
+ # 从元数据中提取情景信息
+ session_id = memory_item.metadata.get("session_id", "default_session")
+ context = memory_item.metadata.get("context", {})
+ outcome = memory_item.metadata.get("outcome")
+ participants = memory_item.metadata.get("participants", [])
+ tags = memory_item.metadata.get("tags", [])
+
+ # 创建情景(内存缓存)
+ episode = Episode(
+ episode_id=memory_item.id,
+ user_id=memory_item.user_id,
+ session_id=session_id,
+ timestamp=memory_item.timestamp,
+ content=memory_item.content,
+ context=context,
+ outcome=outcome,
+ importance=memory_item.importance
+ )
+ self.episodes.append(episode)
+ if session_id not in self.sessions:
+ self.sessions[session_id] = []
+ self.sessions[session_id].append(episode.episode_id)
+
+ # 1) 权威存储(SQLite)
+ ts_int = int(memory_item.timestamp.timestamp())
+ self.doc_store.add_memory(
+ memory_id=memory_item.id,
+ user_id=memory_item.user_id,
+ content=memory_item.content,
+ memory_type="episodic",
+ timestamp=ts_int,
+ importance=memory_item.importance,
+ properties={
+ "session_id": session_id,
+ "context": context,
+ "outcome": outcome,
+ "participants": participants,
+ "tags": tags
+ }
+ )
+
+ # 2) 向量索引(Qdrant)
+ try:
+ embedding = self.embedder.encode(memory_item.content)
+ if hasattr(embedding, "tolist"):
+ embedding = embedding.tolist()
+ self.vector_store.add_vectors(
+ vectors=[embedding],
+ metadata=[{
+ "memory_id": memory_item.id,
+ "user_id": memory_item.user_id,
+ "memory_type": "episodic",
+ "importance": memory_item.importance,
+ "session_id": session_id,
+ "content": memory_item.content
+ }],
+ ids=[memory_item.id]
+ )
+ except Exception:
+ # 向量入库失败不影响权威存储
+ pass
+
+ return memory_item.id
+
+ def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
+ """检索情景记忆(SQLite 结构化检索 + 可选向量检索)"""
+ user_id = kwargs.get("user_id")
+ session_id = kwargs.get("session_id")
+ time_range: Optional[Tuple[datetime, datetime]] = kwargs.get("time_range")
+ importance_threshold: Optional[float] = kwargs.get("importance_threshold")
+
+ # 结构化过滤候选(来自权威库)
+ candidate_ids: Optional[set] = None
+ if time_range is not None or importance_threshold is not None:
+ start_ts = int(time_range[0].timestamp()) if time_range else None
+ end_ts = int(time_range[1].timestamp()) if time_range else None
+ docs = self.doc_store.search_memories(
+ user_id=user_id,
+ memory_type="episodic",
+ start_time=start_ts,
+ end_time=end_ts,
+ importance_threshold=importance_threshold,
+ limit=1000
+ )
+ candidate_ids = {d["memory_id"] for d in docs}
+
+ # 若禁用向量检索(或不可用),直接走 SQLite 文本检索(持久化可用)
+ if self.embedder is None or self.vector_store is None:
+ docs = self.doc_store.search_memories(
+ user_id=user_id,
+ memory_type="episodic",
+ text_query=query,
+ start_time=int(time_range[0].timestamp()) if time_range else None,
+ end_time=int(time_range[1].timestamp()) if time_range else None,
+ importance_threshold=importance_threshold,
+ limit=max(limit * 5, 20),
+ )
+ out: List[Tuple[float, MemoryItem]] = []
+ now_ts = int(datetime.now().timestamp())
+ for doc in docs:
+ if candidate_ids is not None and doc["memory_id"] not in candidate_ids:
+ continue
+ if session_id and (doc.get("properties", {}) or {}).get("session_id") != session_id:
+ continue
+ age_days = max(0.0, (now_ts - int(doc["timestamp"])) / 86400.0)
+ recency_score = 1.0 / (1.0 + age_days)
+ imp = float(doc.get("importance", 0.5))
+ # 文本检索回退:相似度弱,主要依赖重要性与近因
+ base_relevance = 0.4 * 0.8 + recency_score * 0.2
+ importance_weight = 0.8 + (imp * 0.4)
+ combined = base_relevance * importance_weight
+ out.append(
+ (
+ combined,
+ MemoryItem(
+ id=doc["memory_id"],
+ content=doc["content"],
+ memory_type=doc["memory_type"],
+ user_id=doc["user_id"],
+ timestamp=datetime.fromtimestamp(doc["timestamp"]),
+ importance=doc.get("importance", 0.5),
+ metadata={
+ **(doc.get("properties", {}) or {}),
+ "relevance_score": combined,
+ "recency_score": recency_score,
+ "retrieval": "sqlite_like",
+ },
+ ),
+ )
+ )
+ out.sort(key=lambda x: x[0], reverse=True)
+ return [it for _, it in out[:limit]]
+
+ hits = []
+ # 向量检索(Qdrant):可选
+ if self.embedder is not None and self.vector_store is not None:
+ try:
+ query_vec = self.embedder.encode(query)
+ if hasattr(query_vec, "tolist"):
+ query_vec = query_vec.tolist()
+ where = {"memory_type": "episodic"}
+ if user_id:
+ where["user_id"] = user_id
+ hits = self.vector_store.search_similar(
+ query_vector=query_vec,
+ limit=max(limit * 5, 20),
+ where=where,
+ )
+ except Exception:
+ hits = []
+
+ # 过滤与重排
+ now_ts = int(datetime.now().timestamp())
+ results: List[Tuple[float, MemoryItem]] = []
+ seen = set()
+ for hit in hits:
+ meta = hit.get("metadata", {})
+ mem_id = meta.get("memory_id")
+ if not mem_id or mem_id in seen:
+ continue
+
+ # 检查是否已遗忘
+ episode = next((e for e in self.episodes if e.episode_id == mem_id), None)
+ if episode and episode.context.get("forgotten", False):
+ continue # 跳过已遗忘的记忆
+
+ if candidate_ids is not None and mem_id not in candidate_ids:
+ continue
+ if session_id and meta.get("session_id") != session_id:
+ continue
+
+ # 从权威库读取完整记录
+ doc = self.doc_store.get_memory(mem_id)
+ if not doc:
+ continue
+
+ # 计算综合分数:向量0.6 + 近因0.2 + 重要性0.2
+ vec_score = float(hit.get("score", 0.0))
+ age_days = max(0.0, (now_ts - int(doc["timestamp"])) / 86400.0)
+ recency_score = 1.0 / (1.0 + age_days)
+ imp = float(doc.get("importance", 0.5))
+
+ # 新评分算法:向量检索纯基于相似度,重要性作为加权因子
+ # 基础相似度得分(不受重要性影响)
+ base_relevance = vec_score * 0.8 + recency_score * 0.2
+
+ # 重要性作为乘法加权因子,范围 [0.8, 1.2]
+ importance_weight = 0.8 + (imp * 0.4)
+
+ # 最终得分:相似度 * 重要性权重
+ combined = base_relevance * importance_weight
+
+ item = MemoryItem(
+ id=doc["memory_id"],
+ content=doc["content"],
+ memory_type=doc["memory_type"],
+ user_id=doc["user_id"],
+ timestamp=datetime.fromtimestamp(doc["timestamp"]),
+ importance=doc.get("importance", 0.5),
+ metadata={
+ **doc.get("properties", {}),
+ "relevance_score": combined,
+ "vector_score": vec_score,
+ "recency_score": recency_score
+ }
+ )
+ results.append((combined, item))
+ seen.add(mem_id)
+
+ # 若向量检索无结果,回退到简单关键词匹配(内存缓存)
+ if not results:
+ query_lower = query.lower()
+ for ep in self._filter_episodes(user_id, session_id, time_range):
+ if query_lower in ep.content.lower():
+ recency_score = 1.0 / (1.0 + max(0.0, (now_ts - int(ep.timestamp.timestamp())) / 86400.0))
+ # 回退匹配:新评分算法
+ keyword_score = 0.5 # 简单关键词匹配的基础分数
+ base_relevance = keyword_score * 0.8 + recency_score * 0.2
+ importance_weight = 0.8 + (ep.importance * 0.4)
+ combined = base_relevance * importance_weight
+ item = MemoryItem(
+ id=ep.episode_id,
+ content=ep.content,
+ memory_type="episodic",
+ user_id=ep.user_id,
+ timestamp=ep.timestamp,
+ importance=ep.importance,
+ metadata={
+ "session_id": ep.session_id,
+ "context": ep.context,
+ "outcome": ep.outcome,
+ "relevance_score": combined
+ }
+ )
+ results.append((combined, item))
+
+ results.sort(key=lambda x: x[0], reverse=True)
+ return [it for _, it in results[:limit]]
+
+ def update(
+ self,
+ memory_id: str,
+ content: str = None,
+ importance: float = None,
+ metadata: Dict[str, Any] = None
+ ) -> bool:
+ """更新情景记忆(SQLite为权威,Qdrant按需重嵌入)"""
+ updated = False
+ for episode in self.episodes:
+ if episode.episode_id == memory_id:
+ if content is not None:
+ episode.content = content
+ if importance is not None:
+ episode.importance = importance
+ if metadata is not None:
+ episode.context.update(metadata.get("context", {}))
+ if "outcome" in metadata:
+ episode.outcome = metadata["outcome"]
+ updated = True
+ break
+
+ # 更新SQLite
+ doc_updated = self.doc_store.update_memory(
+ memory_id=memory_id,
+ content=content,
+ importance=importance,
+ properties=metadata
+ )
+
+ # 如内容变更,重嵌入并upsert到Qdrant
+ if content is not None and self.embedder is not None and self.vector_store is not None:
+ try:
+ embedding = self.embedder.encode(content)
+ if hasattr(embedding, "tolist"):
+ embedding = embedding.tolist()
+ # 获取更新后的记录以同步payload
+ doc = self.doc_store.get_memory(memory_id)
+ payload = {
+ "memory_id": memory_id,
+ "user_id": doc["user_id"] if doc else "",
+ "memory_type": "episodic",
+ "importance": (doc.get("importance") if doc else importance) or 0.5,
+ "session_id": (doc.get("properties", {}) or {}).get("session_id"),
+ "content": content
+ }
+ self.vector_store.add_vectors(
+ vectors=[embedding],
+ metadata=[payload],
+ ids=[memory_id]
+ )
+ except Exception:
+ pass
+
+ return updated or doc_updated
+
+ def remove(self, memory_id: str) -> bool:
+ """删除情景记忆(SQLite + Qdrant)"""
+ removed = False
+ for i, episode in enumerate(self.episodes):
+ if episode.episode_id == memory_id:
+ removed_episode = self.episodes.pop(i)
+ session_id = removed_episode.session_id
+ if session_id in self.sessions:
+ self.sessions[session_id].remove(memory_id)
+ if not self.sessions[session_id]:
+ del self.sessions[session_id]
+ removed = True
+ break
+
+ # 权威库删除
+ doc_deleted = self.doc_store.delete_memory(memory_id)
+
+ # 向量库删除
+ if self.vector_store is not None:
+ try:
+ self.vector_store.delete_memories([memory_id])
+ except Exception:
+ pass
+
+ return removed or doc_deleted
+
+ def has_memory(self, memory_id: str) -> bool:
+ """检查记忆是否存在"""
+ return any(episode.episode_id == memory_id for episode in self.episodes)
+
+ def clear(self):
+ """清空所有情景记忆(仅清理episodic,不影响其他类型)"""
+ # 内存缓存
+ self.episodes.clear()
+ self.sessions.clear()
+ self.patterns_cache.clear()
+
+ # SQLite内的episodic全部删除
+ docs = self.doc_store.search_memories(memory_type="episodic", limit=10000)
+ ids = [d["memory_id"] for d in docs]
+ for mid in ids:
+ self.doc_store.delete_memory(mid)
+
+ # Qdrant按ID删除对应向量
+ try:
+ if ids:
+ self.vector_store.delete_memories(ids)
+ except Exception:
+ pass
+
+ def forget(self, strategy: str = "importance_based", threshold: float = 0.1, max_age_days: int = 30) -> int:
+ """情景记忆遗忘机制(硬删除)"""
+ forgotten_count = 0
+ current_time = datetime.now()
+
+ to_remove = [] # 收集要删除的记忆ID
+
+ for episode in self.episodes:
+ should_forget = False
+
+ if strategy == "importance_based":
+ # 基于重要性遗忘
+ if episode.importance < threshold:
+ should_forget = True
+ elif strategy == "time_based":
+ # 基于时间遗忘
+ cutoff_time = current_time - timedelta(days=max_age_days)
+ if episode.timestamp < cutoff_time:
+ should_forget = True
+ elif strategy == "capacity_based":
+ # 基于容量遗忘(保留最重要的)
+ if len(self.episodes) > self.config.max_capacity:
+ sorted_episodes = sorted(self.episodes, key=lambda e: e.importance)
+ excess_count = len(self.episodes) - self.config.max_capacity
+ if episode in sorted_episodes[:excess_count]:
+ should_forget = True
+
+ if should_forget:
+ to_remove.append(episode.episode_id)
+
+ # 执行硬删除
+ for episode_id in to_remove:
+ if self.remove(episode_id):
+ forgotten_count += 1
+ logger.info(f"情景记忆硬删除: {episode_id[:8]}... (策略: {strategy})")
+
+ return forgotten_count
+
+ def get_all(self) -> List[MemoryItem]:
+ """获取所有情景记忆(转换为MemoryItem格式)"""
+ memory_items = []
+ for episode in self.episodes:
+ memory_item = MemoryItem(
+ id=episode.episode_id,
+ content=episode.content,
+ memory_type="episodic",
+ user_id=episode.user_id,
+ timestamp=episode.timestamp,
+ importance=episode.importance,
+ metadata=episode.metadata
+ )
+ memory_items.append(memory_item)
+ return memory_items
+
+ def get_stats(self) -> Dict[str, Any]:
+ """获取情景记忆统计信息(合并SQLite与Qdrant)"""
+ # 硬删除模式:所有episodes都是活跃的
+ active_episodes = self.episodes
+
+ db_stats = self.doc_store.get_database_stats()
+ try:
+ vs_stats = self.vector_store.get_collection_stats()
+ except Exception:
+ vs_stats = {"store_type": "qdrant"}
+ return {
+ "count": len(active_episodes), # 活跃记忆数量
+ "forgotten_count": 0, # 硬删除模式下已遗忘的记忆会被直接删除
+ "total_count": len(self.episodes), # 总记忆数量
+ "sessions_count": len(self.sessions),
+ "avg_importance": sum(e.importance for e in active_episodes) / len(active_episodes) if active_episodes else 0.0,
+ "time_span_days": self._calculate_time_span(),
+ "memory_type": "episodic",
+ "vector_store": vs_stats,
+ "document_store": {k: v for k, v in db_stats.items() if k.endswith("_count") or k in ["store_type", "db_path"]}
+ }
+
+ def get_session_episodes(self, session_id: str) -> List[Episode]:
+ """获取指定会话的所有情景"""
+ if session_id not in self.sessions:
+ return []
+
+ episode_ids = self.sessions[session_id]
+ return [e for e in self.episodes if e.episode_id in episode_ids]
+
+ def find_patterns(self, user_id: str = None, min_frequency: int = 2) -> List[Dict[str, Any]]:
+ """发现用户行为模式"""
+ # 检查缓存
+ cache_key = f"{user_id}_{min_frequency}"
+ if (cache_key in self.patterns_cache and
+ self.last_pattern_analysis and
+ (datetime.now() - self.last_pattern_analysis).hours < 1):
+ return self.patterns_cache[cache_key]
+
+ # 过滤情景
+ episodes = [e for e in self.episodes if user_id is None or e.user_id == user_id]
+
+ # 简单的模式识别:基于内容关键词
+ keyword_patterns = {}
+ context_patterns = {}
+
+ for episode in episodes:
+ # 提取关键词
+ words = episode.content.lower().split()
+ for word in words:
+ if len(word) > 3: # 忽略短词
+ keyword_patterns[word] = keyword_patterns.get(word, 0) + 1
+
+ # 提取上下文模式
+ for key, value in episode.context.items():
+ pattern_key = f"{key}:{value}"
+ context_patterns[pattern_key] = context_patterns.get(pattern_key, 0) + 1
+
+ # 筛选频繁模式
+ patterns = []
+
+ for keyword, frequency in keyword_patterns.items():
+ if frequency >= min_frequency:
+ patterns.append({
+ "type": "keyword",
+ "pattern": keyword,
+ "frequency": frequency,
+ "confidence": frequency / len(episodes)
+ })
+
+ for context_pattern, frequency in context_patterns.items():
+ if frequency >= min_frequency:
+ patterns.append({
+ "type": "context",
+ "pattern": context_pattern,
+ "frequency": frequency,
+ "confidence": frequency / len(episodes)
+ })
+
+ # 按频率排序
+ patterns.sort(key=lambda x: x["frequency"], reverse=True)
+
+ # 缓存结果
+ self.patterns_cache[cache_key] = patterns
+ self.last_pattern_analysis = datetime.now()
+
+ return patterns
+
+ def get_timeline(self, user_id: str = None, limit: int = 50) -> List[Dict[str, Any]]:
+ """获取时间线视图"""
+ episodes = [e for e in self.episodes if user_id is None or e.user_id == user_id]
+ episodes.sort(key=lambda x: x.timestamp, reverse=True)
+
+ timeline = []
+ for episode in episodes[:limit]:
+ timeline.append({
+ "episode_id": episode.episode_id,
+ "timestamp": episode.timestamp.isoformat(),
+ "content": episode.content[:100] + "..." if len(episode.content) > 100 else episode.content,
+ "session_id": episode.session_id,
+ "importance": episode.importance,
+ "outcome": episode.outcome
+ })
+
+ return timeline
+
+ def _filter_episodes(
+ self,
+ user_id: str = None,
+ session_id: str = None,
+ time_range: Tuple[datetime, datetime] = None
+ ) -> List[Episode]:
+ """过滤情景"""
+ filtered = self.episodes
+
+ if user_id:
+ filtered = [e for e in filtered if e.user_id == user_id]
+
+ if session_id:
+ filtered = [e for e in filtered if e.session_id == session_id]
+
+ if time_range:
+ start_time, end_time = time_range
+ filtered = [e for e in filtered if start_time <= e.timestamp <= end_time]
+
+ return filtered
+
+ def _calculate_time_span(self) -> float:
+ """计算记忆时间跨度(天)"""
+ if not self.episodes:
+ return 0.0
+
+ timestamps = [e.timestamp for e in self.episodes]
+ min_time = min(timestamps)
+ max_time = max(timestamps)
+
+ return (max_time - min_time).days
+
+ def _persist_episode(self, episode: Episode):
+ """持久化情景到存储后端"""
+ if self.storage and hasattr(self.storage, 'add_memory'):
+ self.storage.add_memory(
+ memory_id=episode.episode_id,
+ user_id=episode.user_id,
+ content=episode.content,
+ memory_type="episodic",
+ timestamp=int(episode.timestamp.timestamp()),
+ importance=episode.importance,
+ properties={
+ "session_id": episode.session_id,
+ "context": episode.context,
+ "outcome": episode.outcome
+ }
+ )
+
+ def _remove_from_storage(self, memory_id: str):
+ """从存储后端删除"""
+ if self.storage and hasattr(self.storage, 'delete_memory'):
+ self.storage.delete_memory(memory_id)
diff --git a/Co-creation-projects/aug618-Praxis/memory/types/perceptual.py b/Co-creation-projects/aug618-Praxis/memory/types/perceptual.py
new file mode 100644
index 00000000..a8458913
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/types/perceptual.py
@@ -0,0 +1,709 @@
+"""感知记忆实现(长存的多模态)
+
+按照第8章架构设计的感知记忆(长期、多模态),提供:
+- 多模态数据存储(文本、图像、音频等)
+- 结构化元数据 + 向量索引(SQLite + Qdrant)
+- 同模态检索(跨模态在无CLIP/CLAP依赖时有限)
+- 懒加载编码:文本用 sentence-transformers;图像/音频用轻量确定性哈希向量
+"""
+
+from typing import List, Dict, Any, Optional, Union, Tuple
+from datetime import datetime, timedelta
+import hashlib
+import os
+import random
+import logging
+
+logger = logging.getLogger(__name__)
+
+from ..base import BaseMemory, MemoryItem, MemoryConfig
+from ..storage import SQLiteDocumentStore, QdrantVectorStore
+from ..embedding import get_text_embedder, get_dimension
+
+class Perception:
+ """感知数据实体"""
+
+ def __init__(
+ self,
+ perception_id: str,
+ data: Any,
+ modality: str,
+ encoding: Optional[List[float]] = None,
+ metadata: Dict[str, Any] = None
+ ):
+ self.perception_id = perception_id
+ self.data = data
+ self.modality = modality # text, image, audio, video, structured
+ self.encoding = encoding or []
+ self.metadata = metadata or {}
+ self.timestamp = datetime.now()
+ self.data_hash = self._calculate_hash()
+
+ def _calculate_hash(self) -> str:
+ """计算数据哈希"""
+ if isinstance(self.data, str):
+ return hashlib.md5(self.data.encode()).hexdigest()
+ elif isinstance(self.data, bytes):
+ return hashlib.md5(self.data).hexdigest()
+ else:
+ return hashlib.md5(str(self.data).encode()).hexdigest()
+
+class PerceptualMemory(BaseMemory):
+ """感知记忆实现
+
+ 特点:
+ - 支持多模态数据(文本、图像、音频等)
+ - 跨模态相似性搜索
+ - 感知数据的语义理解
+ - 支持内容生成和检索
+ """
+
+ def __init__(self, config: MemoryConfig, storage_backend=None):
+ super().__init__(config, storage_backend)
+
+ # 感知数据存储(内存缓存)
+ self.perceptions: Dict[str, Perception] = {}
+ self.perceptual_memories: List[MemoryItem] = []
+
+ # 模态索引
+ self.modality_index: Dict[str, List[str]] = {} # modality -> perception_ids
+
+ # 支持的模态
+ self.supported_modalities = set(self.config.perceptual_memory_modalities)
+
+ # 文档权威存储(SQLite)
+ db_dir = getattr(self.config, 'storage_path', "./memory_data")
+ os.makedirs(db_dir, exist_ok=True)
+ db_path = os.path.join(db_dir, "memory.db")
+ self.doc_store = SQLiteDocumentStore(db_path=db_path)
+
+ # 嵌入维度(与统一文本嵌入保持一致)
+ self.text_embedder = get_text_embedder()
+ self.vector_dim = get_dimension(getattr(self.text_embedder, 'dimension', 384))
+
+ # 可选加载:图像CLIP与音频CLAP(缺依赖则优雅降级为哈希编码)
+ self._clip_model = None
+ self._clip_processor = None
+ self._clap_model = None
+ self._clap_processor = None
+ self._image_dim = None
+ self._audio_dim = None
+ try:
+ from transformers import CLIPModel, CLIPProcessor
+ clip_name = os.getenv("CLIP_MODEL", "openai/clip-vit-base-patch32")
+ self._clip_model = CLIPModel.from_pretrained(clip_name)
+ self._clip_processor = CLIPProcessor.from_pretrained(clip_name)
+ # 估计输出维度
+ self._image_dim = self._clip_model.config.projection_dim if hasattr(self._clip_model.config, 'projection_dim') else 512
+ except Exception:
+ self._clip_model = None
+ self._clip_processor = None
+ self._image_dim = self.vector_dim
+ try:
+ from transformers import ClapProcessor, ClapModel
+ clap_name = os.getenv("CLAP_MODEL", "laion/clap-htsat-unfused")
+ self._clap_model = ClapModel.from_pretrained(clap_name)
+ self._clap_processor = ClapProcessor.from_pretrained(clap_name)
+ # 估计输出维度
+ self._audio_dim = getattr(self._clap_model.config, 'projection_dim', None) or 512
+ except Exception:
+ self._clap_model = None
+ self._clap_processor = None
+ self._audio_dim = self.vector_dim
+
+ # 向量存储(Qdrant)— 按模态拆分集合,避免维度冲突,使用连接管理器避免重复连接
+ from ..storage.qdrant_store import QdrantConnectionManager
+ qdrant_url = os.getenv("QDRANT_URL")
+ qdrant_api_key = os.getenv("QDRANT_API_KEY")
+ base_collection = os.getenv("QDRANT_COLLECTION", "hello_agents_vectors")
+ distance = os.getenv("QDRANT_DISTANCE", "cosine")
+
+ self.vector_stores: Dict[str, QdrantVectorStore] = {}
+ # 文本集合
+ self.vector_stores["text"] = QdrantConnectionManager.get_instance(
+ url=qdrant_url,
+ api_key=qdrant_api_key,
+ collection_name=f"{base_collection}_perceptual_text",
+ vector_size=self.vector_dim,
+ distance=distance
+ )
+ # 图像集合(若CLIP不可用,维度退化为text维度)
+ self.vector_stores["image"] = QdrantConnectionManager.get_instance(
+ url=qdrant_url,
+ api_key=qdrant_api_key,
+ collection_name=f"{base_collection}_perceptual_image",
+ vector_size=int(self._image_dim or self.vector_dim),
+ distance=distance
+ )
+ # 音频集合(若CLAP不可用,维度退化为text维度)
+ self.vector_stores["audio"] = QdrantConnectionManager.get_instance(
+ url=qdrant_url,
+ api_key=qdrant_api_key,
+ collection_name=f"{base_collection}_perceptual_audio",
+ vector_size=int(self._audio_dim or self.vector_dim),
+ distance=distance
+ )
+
+ # 编码器(轻量实现;真实场景可替换为CLIP/CLAP等)
+ self.encoders = self._init_encoders()
+
+ def add(self, memory_item: MemoryItem) -> str:
+ """添加感知记忆(SQLite权威 + Qdrant向量)"""
+ modality = memory_item.metadata.get("modality", "text")
+ raw_data = memory_item.metadata.get("raw_data", memory_item.content)
+ if modality not in self.supported_modalities:
+ raise ValueError(f"不支持的模态类型: {modality}")
+
+ # 编码感知数据
+ perception = self._encode_perception(raw_data, modality, memory_item.id)
+
+ # 缓存与索引
+ self.perceptions[perception.perception_id] = perception
+ if modality not in self.modality_index:
+ self.modality_index[modality] = []
+ self.modality_index[modality].append(perception.perception_id)
+
+ # 存储记忆项(缓存)
+ memory_item.metadata["perception_id"] = perception.perception_id
+ memory_item.metadata["modality"] = modality
+ # 不把大向量放到metadata中,避免膨胀
+ self.perceptual_memories.append(memory_item)
+
+ # 1) SQLite 权威入库
+ ts_int = int(memory_item.timestamp.timestamp())
+ self.doc_store.add_memory(
+ memory_id=memory_item.id,
+ user_id=memory_item.user_id,
+ content=memory_item.content,
+ memory_type="perceptual",
+ timestamp=ts_int,
+ importance=memory_item.importance,
+ properties={
+ "perception_id": perception.perception_id,
+ "modality": modality,
+ "context": memory_item.metadata.get("context", {}),
+ "tags": memory_item.metadata.get("tags", []),
+ }
+ )
+
+ # 2) Qdrant 向量入库(按模态写入对应集合)
+ try:
+ vector = perception.encoding
+ store = self._get_vector_store_for_modality(modality)
+ store.add_vectors(
+ vectors=[vector],
+ metadata=[{
+ "memory_id": memory_item.id,
+ "user_id": memory_item.user_id,
+ "memory_type": "perceptual",
+ "modality": modality,
+ "importance": memory_item.importance,
+ "content": memory_item.content,
+ }],
+ ids=[memory_item.id]
+ )
+ except Exception:
+ pass
+
+ return memory_item.id
+
+ def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
+ """检索感知记忆(可筛模态;同模态向量检索+时间/重要性融合)"""
+ user_id = kwargs.get("user_id")
+ target_modality = kwargs.get("target_modality") # 可选:限制目标模态
+ query_modality = kwargs.get("query_modality", target_modality or "text")
+
+ # 仅在同模态情况下进行向量检索(跨模态需要CLIP/CLAP,此处保留简单回退)
+ try:
+ qvec = self._encode_data(query, query_modality)
+ where = {"memory_type": "perceptual"}
+ if user_id:
+ where["user_id"] = user_id
+ if target_modality:
+ where["modality"] = target_modality
+ store = self._get_vector_store_for_modality(target_modality or query_modality)
+ hits = store.search_similar(
+ query_vector=qvec,
+ limit=max(limit * 5, 20),
+ where=where
+ )
+ except Exception:
+ hits = []
+
+ # 融合排序
+ now_ts = int(datetime.now().timestamp())
+ results: List[Tuple[float, MemoryItem]] = []
+ seen = set()
+ for hit in hits:
+ meta = hit.get("metadata", {})
+ mem_id = meta.get("memory_id")
+ if not mem_id or mem_id in seen:
+ continue
+ if target_modality and meta.get("modality") != target_modality:
+ continue
+ doc = self.doc_store.get_memory(mem_id)
+ if not doc:
+ continue
+ vec_score = float(hit.get("score", 0.0))
+ age_days = max(0.0, (now_ts - int(doc["timestamp"])) / 86400.0)
+ recency_score = 1.0 / (1.0 + age_days)
+ imp = float(doc.get("importance", 0.5))
+
+ # 新评分算法:向量检索纯基于相似度,重要性作为加权因子
+ # 基础相似度得分(不受重要性影响)
+ base_relevance = vec_score * 0.8 + recency_score * 0.2
+
+ # 重要性作为乘法加权因子,范围 [0.8, 1.2]
+ importance_weight = 0.8 + (imp * 0.4)
+
+ # 最终得分:相似度 * 重要性权重
+ combined = base_relevance * importance_weight
+
+ item = MemoryItem(
+ id=doc["memory_id"],
+ content=doc["content"],
+ memory_type=doc["memory_type"],
+ user_id=doc["user_id"],
+ timestamp=datetime.fromtimestamp(doc["timestamp"]),
+ importance=imp,
+ metadata={**doc.get("properties", {}), "relevance_score": combined,
+ "vector_score": vec_score, "recency_score": recency_score}
+ )
+ results.append((combined, item))
+ seen.add(mem_id)
+
+ # 简单回退:若无命中且有目标模态,则按SQLite结构化过滤+关键词兜底
+ if not results:
+ for m in self.perceptual_memories:
+ if target_modality and m.metadata.get("modality") != target_modality:
+ continue
+ if query.lower() in (m.content or "").lower():
+ recency_score = 1.0 / (1.0 + max(0.0, (now_ts - int(m.timestamp.timestamp())) / 86400.0))
+ # 回退匹配:新评分算法
+ keyword_score = 0.5 # 简单关键词匹配的基础分数
+ base_relevance = keyword_score * 0.8 + recency_score * 0.2
+ importance_weight = 0.8 + (m.importance * 0.4)
+ combined = base_relevance * importance_weight
+ results.append((combined, m))
+
+ results.sort(key=lambda x: x[0], reverse=True)
+ return [it for _, it in results[:limit]]
+
+ def update(
+ self,
+ memory_id: str,
+ content: str = None,
+ importance: float = None,
+ metadata: Dict[str, Any] = None
+ ) -> bool:
+ """更新感知记忆"""
+ updated = False
+ modality_cache = None
+ for memory in self.perceptual_memories:
+ if memory.id == memory_id:
+ if content is not None:
+ memory.content = content
+ if importance is not None:
+ memory.importance = importance
+ if metadata is not None:
+ memory.metadata.update(metadata)
+ modality_cache = memory.metadata.get("modality", "text")
+ updated = True
+ break
+
+ # 更新SQLite
+ self.doc_store.update_memory(
+ memory_id=memory_id,
+ content=content,
+ importance=importance,
+ properties=metadata
+ )
+
+ # 如内容或原始数据改变,则重嵌入并upsert到Qdrant
+ if content is not None or (metadata and "raw_data" in metadata):
+ modality = metadata.get("modality", modality_cache or "text") if metadata else (modality_cache or "text")
+ raw = metadata.get("raw_data", content) if metadata else content
+ try:
+ perception = self._encode_perception(raw or "", modality, memory_id)
+ payload = self.doc_store.get_memory(memory_id) or {}
+ self.vector_store.add_vectors(
+ vectors=[perception.encoding],
+ metadata=[{
+ "memory_id": memory_id,
+ "user_id": payload.get("user_id", ""),
+ "memory_type": "perceptual",
+ "modality": modality,
+ "importance": (payload.get("importance") or importance) or 0.5,
+ "content": content or (payload.get("content", "")),
+ }],
+ ids=[memory_id]
+ )
+ except Exception:
+ pass
+
+ return updated
+
+ def remove(self, memory_id: str) -> bool:
+ """删除感知记忆"""
+ removed = False
+ for i, memory in enumerate(self.perceptual_memories):
+ if memory.id == memory_id:
+ removed_memory = self.perceptual_memories.pop(i)
+ perception_id = removed_memory.metadata.get("perception_id")
+ if perception_id and perception_id in self.perceptions:
+ perception = self.perceptions.pop(perception_id)
+ modality = perception.modality
+ if modality in self.modality_index:
+ if perception_id in self.modality_index[modality]:
+ self.modality_index[modality].remove(perception_id)
+ if not self.modality_index[modality]:
+ del self.modality_index[modality]
+ removed = True
+ break
+
+ # 权威库删除
+ self.doc_store.delete_memory(memory_id)
+ # 向量库删除(所有模态集合尝试删除)
+ for store in self.vector_stores.values():
+ try:
+ store.delete_memories([memory_id])
+ except Exception:
+ pass
+
+ return removed
+
+ def has_memory(self, memory_id: str) -> bool:
+ """检查记忆是否存在"""
+ return any(memory.id == memory_id for memory in self.perceptual_memories)
+
+ def forget(self, strategy: str = "importance_based", threshold: float = 0.1, max_age_days: int = 30) -> int:
+ """感知记忆遗忘机制(硬删除)"""
+ forgotten_count = 0
+ current_time = datetime.now()
+
+ to_remove = [] # 收集要删除的记忆ID
+
+ for memory in self.perceptual_memories:
+ should_forget = False
+
+ if strategy == "importance_based":
+ # 基于重要性遗忘
+ if memory.importance < threshold:
+ should_forget = True
+ elif strategy == "time_based":
+ # 基于时间遗忘
+ cutoff_time = current_time - timedelta(days=max_age_days)
+ if memory.timestamp < cutoff_time:
+ should_forget = True
+ elif strategy == "capacity_based":
+ # 基于容量遗忘(保留最重要的)
+ if len(self.perceptual_memories) > self.config.max_capacity:
+ sorted_memories = sorted(self.perceptual_memories, key=lambda m: m.importance)
+ excess_count = len(self.perceptual_memories) - self.config.max_capacity
+ if memory in sorted_memories[:excess_count]:
+ should_forget = True
+
+ if should_forget:
+ to_remove.append(memory.id)
+
+ # 执行硬删除
+ for memory_id in to_remove:
+ if self.remove(memory_id):
+ forgotten_count += 1
+ logger.info(f"感知记忆硬删除: {memory_id[:8]}... (策略: {strategy})")
+
+ return forgotten_count
+
+ def clear(self):
+ """清空所有感知记忆"""
+ self.perceptual_memories.clear()
+ self.perceptions.clear()
+ self.modality_index.clear()
+ # 删除SQLite中的perceptual记录
+ docs = self.doc_store.search_memories(memory_type="perceptual", limit=10000)
+ ids = [d["memory_id"] for d in docs]
+ for mid in ids:
+ self.doc_store.delete_memory(mid)
+ # 删除Qdrant向量(所有模态集合)
+ for store in self.vector_stores.values():
+ try:
+ if ids:
+ store.delete_memories(ids)
+ except Exception:
+ pass
+
+ def get_all(self) -> List[MemoryItem]:
+ """获取所有感知记忆"""
+ return self.perceptual_memories.copy()
+
+ def get_stats(self) -> Dict[str, Any]:
+ """获取感知记忆统计信息"""
+ # 硬删除模式:所有记忆都是活跃的
+ active_memories = self.perceptual_memories
+
+ modality_counts = {modality: len(ids) for modality, ids in self.modality_index.items()}
+ vs_stats_all = {}
+ for mod, store in self.vector_stores.items():
+ try:
+ vs_stats_all[mod] = store.get_collection_stats()
+ except Exception:
+ vs_stats_all[mod] = {"store_type": "qdrant"}
+ db_stats = self.doc_store.get_database_stats()
+
+ return {
+ "count": len(active_memories), # 活跃记忆数量
+ "forgotten_count": 0, # 硬删除模式下已遗忘的记忆会被直接删除
+ "total_count": len(self.perceptual_memories), # 总记忆数量
+ "perceptions_count": len(self.perceptions),
+ "modality_counts": modality_counts,
+ "supported_modalities": list(self.supported_modalities),
+ "avg_importance": sum(m.importance for m in active_memories) / len(active_memories) if active_memories else 0.0,
+ "memory_type": "perceptual",
+ "vector_stores": vs_stats_all,
+ "document_store": {k: v for k, v in db_stats.items() if k.endswith("_count") or k in ["store_type", "db_path"]}
+ }
+
+ def cross_modal_search(
+ self,
+ query: Any,
+ query_modality: str,
+ target_modality: str = None,
+ limit: int = 5
+ ) -> List[MemoryItem]:
+ """跨模态搜索"""
+ return self.retrieve(
+ query=str(query),
+ limit=limit,
+ query_modality=query_modality,
+ target_modality=target_modality
+ )
+
+ def get_by_modality(self, modality: str, limit: int = 10) -> List[MemoryItem]:
+ """按模态获取记忆"""
+ if modality not in self.modality_index:
+ return []
+
+ perception_ids = self.modality_index[modality]
+ results = []
+
+ for memory in self.perceptual_memories:
+ if memory.metadata.get("perception_id") in perception_ids:
+ results.append(memory)
+ if len(results) >= limit:
+ break
+
+ return results
+
+ def generate_content(self, prompt: str, target_modality: str) -> Optional[str]:
+ """基于感知记忆生成内容"""
+ # 简化的内容生成实现
+ # 实际应用中需要使用生成模型
+
+ if target_modality not in self.supported_modalities:
+ return None
+
+ # 检索相关感知记忆
+ relevant_memories = self.retrieve(prompt, limit=3)
+
+ if not relevant_memories:
+ return None
+
+ # 简单的内容组合
+ if target_modality == "text":
+ contents = [memory.content for memory in relevant_memories]
+ return f"基于感知记忆生成的内容:\n" + "\n".join(contents)
+
+ return f"生成的{target_modality}内容(基于{len(relevant_memories)}个相关记忆)"
+
+ def _init_encoders(self) -> Dict[str, Any]:
+ """初始化编码器(轻量、确定性,统一输出self.vector_dim维)"""
+ encoders = {}
+ for modality in self.supported_modalities:
+ if modality == "text":
+ encoders[modality] = self._text_encoder
+ elif modality == "image":
+ encoders[modality] = self._image_encoder
+ elif modality == "audio":
+ encoders[modality] = self._audio_encoder
+ else:
+ encoders[modality] = self._default_encoder
+ return encoders
+
+ def _encode_perception(self, data: Any, modality: str, memory_id: str) -> Perception:
+ """编码感知数据"""
+ encoding = self._encode_data(data, modality)
+
+ perception = Perception(
+ perception_id=f"perception_{memory_id}",
+ data=data,
+ modality=modality,
+ encoding=encoding,
+ metadata={"source": "memory_system"}
+ )
+
+ return perception
+
+ def _encode_data(self, data: Any, modality: str) -> List[float]:
+ """编码数据为固定维度向量(按模态维度对齐)"""
+ target_dim = self._get_dim_for_modality(modality)
+ encoder = self.encoders.get(modality, self._default_encoder)
+ vec = encoder(data)
+ if not isinstance(vec, list):
+ vec = list(vec)
+ if len(vec) < target_dim:
+ vec = vec + [0.0] * (target_dim - len(vec))
+ elif len(vec) > target_dim:
+ vec = vec[:target_dim]
+ return vec
+
+ def _text_encoder(self, text: str) -> List[float]:
+ """文本编码器(使用嵌入模型)"""
+ emb = self.text_embedder.encode(text or "")
+ if hasattr(emb, "tolist"):
+ emb = emb.tolist()
+ return emb
+
+ def _image_encoder_hash(self, image_data: Any) -> List[float]:
+ """图像编码器(轻量确定性哈希向量,跨环境稳定)"""
+ try:
+ if isinstance(image_data, (bytes, bytearray)):
+ data_bytes = bytes(image_data)
+ elif isinstance(image_data, str) and os.path.exists(image_data):
+ with open(image_data, 'rb') as f:
+ data_bytes = f.read()
+ else:
+ data_bytes = str(image_data).encode('utf-8', errors='ignore')
+ hex_str = hashlib.sha256(data_bytes).hexdigest()
+ return self._hash_to_vector(hex_str, self._get_dim_for_modality("image"))
+ except Exception:
+ return self._hash_to_vector(str(image_data), self._get_dim_for_modality("image"))
+
+ def _image_encoder(self, image_data: Any) -> List[float]:
+ """图像编码器(优先CLIP,不可用则哈希)"""
+ if self._clip_model is None or self._clip_processor is None:
+ return self._image_encoder_hash(image_data)
+ try:
+ from PIL import Image
+ if isinstance(image_data, str) and os.path.exists(image_data):
+ image = Image.open(image_data).convert('RGB')
+ elif isinstance(image_data, (bytes, bytearray)):
+ from io import BytesIO
+ image = Image.open(BytesIO(bytes(image_data))).convert('RGB')
+ else:
+ # 退回到哈希
+ return self._image_encoder_hash(image_data)
+ inputs = self._clip_processor(images=image, return_tensors="pt")
+ with self._no_grad():
+ feats = self._clip_model.get_image_features(**inputs)
+ vec = feats[0].detach().cpu().numpy().tolist()
+ return vec
+ except Exception:
+ return self._image_encoder_hash(image_data)
+
+ def _audio_encoder_hash(self, audio_data: Any) -> List[float]:
+ """音频编码器(轻量确定性哈希向量)"""
+ try:
+ if isinstance(audio_data, (bytes, bytearray)):
+ data_bytes = bytes(audio_data)
+ elif isinstance(audio_data, str) and os.path.exists(audio_data):
+ with open(audio_data, 'rb') as f:
+ data_bytes = f.read()
+ else:
+ data_bytes = str(audio_data).encode('utf-8', errors='ignore')
+ hex_str = hashlib.sha256(data_bytes).hexdigest()
+ return self._hash_to_vector(hex_str, self._get_dim_for_modality("audio"))
+ except Exception:
+ return self._hash_to_vector(str(audio_data), self._get_dim_for_modality("audio"))
+
+ def _audio_encoder(self, audio_data: Any) -> List[float]:
+ """音频编码器(优先CLAP,不可用则哈希)"""
+ if self._clap_model is None or self._clap_processor is None:
+ return self._audio_encoder_hash(audio_data)
+ try:
+ import numpy as np
+ # 加载音频(需要 librosa)
+ import librosa
+ if isinstance(audio_data, str) and os.path.exists(audio_data):
+ speech, sr = librosa.load(audio_data, sr=48000, mono=True)
+ elif isinstance(audio_data, (bytes, bytearray)):
+ # 临时文件方式加载
+ import tempfile
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
+ tmp.write(bytes(audio_data))
+ tmp_path = tmp.name
+ speech, sr = librosa.load(tmp_path, sr=48000, mono=True)
+ try:
+ os.remove(tmp_path)
+ except Exception:
+ pass
+ else:
+ return self._audio_encoder_hash(audio_data)
+ inputs = self._clap_processor(audios=speech, sampling_rate=48000, return_tensors="pt")
+ with self._no_grad():
+ feats = self._clap_model.get_audio_features(**inputs)
+ vec = feats[0].detach().cpu().numpy().tolist()
+ return vec
+ except Exception:
+ return self._audio_encoder_hash(audio_data)
+
+ def _default_encoder(self, data: Any) -> List[float]:
+ """默认编码器(退化为文本嵌入或哈希)"""
+ try:
+ return self._text_encoder(str(data))
+ except Exception:
+ return self._hash_to_vector(str(data), self.vector_dim)
+
+ def _calculate_similarity(self, encoding1: List[float], encoding2: List[float]) -> float:
+ """计算编码相似度"""
+ if not encoding1 or not encoding2:
+ return 0.0
+
+ # 确保长度一致
+ min_len = min(len(encoding1), len(encoding2))
+ if min_len == 0:
+ return 0.0
+
+ # 计算余弦相似度
+ dot_product = sum(a * b for a, b in zip(encoding1[:min_len], encoding2[:min_len]))
+ norm1 = sum(a * a for a in encoding1[:min_len]) ** 0.5
+ norm2 = sum(a * a for a in encoding2[:min_len]) ** 0.5
+
+ if norm1 == 0 or norm2 == 0:
+ return 0.0
+
+ return dot_product / (norm1 * norm2)
+
+ def _hash_to_vector(self, data_str: str, dim: int) -> List[float]:
+ """将字符串哈希为固定维度的[0,1]向量(确定性)"""
+ seed = int(hashlib.sha256(data_str.encode("utf-8", errors="ignore")).hexdigest(), 16) % (2**32)
+ rng = random.Random(seed)
+ return [rng.random() for _ in range(dim)]
+
+ class _no_grad:
+ def __enter__(self):
+ try:
+ import torch
+ self.prev = torch.is_grad_enabled()
+ torch.set_grad_enabled(False)
+ except Exception:
+ self.prev = None
+ return self
+ def __exit__(self, exc_type, exc, tb):
+ try:
+ import torch
+ if self.prev is not None:
+ torch.set_grad_enabled(self.prev)
+ except Exception:
+ pass
+
+ def _get_vector_store_for_modality(self, modality: Optional[str]) -> QdrantVectorStore:
+ mod = (modality or "text").lower()
+ return self.vector_stores.get(mod, self.vector_stores["text"])
+
+ def _get_dim_for_modality(self, modality: Optional[str]) -> int:
+ mod = (modality or "text").lower()
+ if mod == "image":
+ return int(self._image_dim or self.vector_dim)
+ if mod == "audio":
+ return int(self._audio_dim or self.vector_dim)
+ return int(self.vector_dim)
diff --git a/Co-creation-projects/aug618-Praxis/memory/types/semantic.py b/Co-creation-projects/aug618-Praxis/memory/types/semantic.py
new file mode 100644
index 00000000..140754ba
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/types/semantic.py
@@ -0,0 +1,1181 @@
+"""语义记忆实现
+
+结合向量检索和知识图谱的混合语义记忆,使用:
+- HuggingFace 中文预训练模型进行文本嵌入
+- 向量相似度检索进行快速初筛
+- 知识图谱进行实体关系推理
+- 混合检索策略优化结果质量
+"""
+
+from typing import List, Dict, Any, Optional, Set, Tuple
+from datetime import datetime, timedelta
+import json
+import logging
+import math
+import numpy as np
+
+from ..base import BaseMemory, MemoryItem, MemoryConfig
+from ..embedding import get_text_embedder, get_dimension
+from core.database_config import get_database_config
+
+# 配置日志
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+class Entity:
+ """实体类"""
+
+ def __init__(
+ self,
+ entity_id: str,
+ name: str,
+ entity_type: str = "MISC",
+ description: str = "",
+ properties: Dict[str, Any] = None
+ ):
+ self.entity_id = entity_id
+ self.name = name
+ self.entity_type = entity_type # PERSON, ORG, PRODUCT, SKILL, CONCEPT等
+ self.description = description
+ self.properties = properties or {}
+ self.created_at = datetime.now()
+ self.updated_at = datetime.now()
+ self.frequency = 1 # 出现频率
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "entity_id": self.entity_id,
+ "name": self.name,
+ "entity_type": self.entity_type,
+ "description": self.description,
+ "properties": self.properties,
+ "frequency": self.frequency
+ }
+
+class Relation:
+ """关系类"""
+
+ def __init__(
+ self,
+ from_entity: str,
+ to_entity: str,
+ relation_type: str,
+ strength: float = 1.0,
+ evidence: str = "",
+ properties: Dict[str, Any] = None
+ ):
+ self.from_entity = from_entity
+ self.to_entity = to_entity
+ self.relation_type = relation_type
+ self.strength = strength
+ self.evidence = evidence # 支持该关系的原文本
+ self.properties = properties or {}
+ self.created_at = datetime.now()
+ self.frequency = 1 # 关系出现频率
+
+ def to_dict(self) -> Dict[str, Any]:
+ return {
+ "from_entity": self.from_entity,
+ "to_entity": self.to_entity,
+ "relation_type": self.relation_type,
+ "strength": self.strength,
+ "evidence": self.evidence,
+ "properties": self.properties,
+ "frequency": self.frequency
+ }
+
+
+class SemanticMemory(BaseMemory):
+ """增强语义记忆实现
+
+ 特点:
+ - 使用HuggingFace中文预训练模型进行文本嵌入
+ - 向量检索进行快速相似度匹配
+ - 知识图谱存储实体和关系
+ - 混合检索策略:向量+图+语义推理
+ """
+
+ def __init__(self, config: MemoryConfig, storage_backend=None):
+ super().__init__(config, storage_backend)
+
+ # 嵌入模型(统一提供)
+ self.embedding_model = None
+ self._init_embedding_model()
+
+ # 专业数据库存储
+ self.vector_store = None
+ self.graph_store = None
+ self._init_databases()
+
+ # 实体和关系缓存 (用于快速访问)
+ self.entities: Dict[str, Entity] = {}
+ self.relations: List[Relation] = []
+
+ # 实体识别器
+ self.nlp = None
+ self._init_nlp()
+
+ # 记忆存储
+ self.semantic_memories: List[MemoryItem] = []
+ self.memory_embeddings: Dict[str, np.ndarray] = {}
+
+ logger.info("增强语义记忆初始化完成(使用Qdrant+Neo4j专业数据库)")
+
+ def _init_embedding_model(self):
+ """初始化统一嵌入模型(由 embedding_provider 管理)。"""
+ try:
+ self.embedding_model = get_text_embedder()
+ # 轻量健康检查与日志
+ try:
+ test_vec = self.embedding_model.encode("health_check")
+ dim = getattr(self.embedding_model, "dimension", len(test_vec))
+ logger.info(f"✅ 嵌入模型就绪,维度: {dim}")
+ except Exception:
+ logger.info("✅ 嵌入模型就绪")
+ except Exception as e:
+ logger.error(f"❌ 嵌入模型初始化失败: {e}")
+ raise
+
+ def _init_databases(self):
+ """初始化专业数据库存储"""
+ try:
+ # 获取数据库配置
+ db_config = get_database_config()
+
+ # 初始化Qdrant向量数据库(使用连接管理器避免重复连接)
+ from ..storage.qdrant_store import QdrantConnectionManager
+ qdrant_config = db_config.get_qdrant_config() or {}
+ qdrant_config["vector_size"] = get_dimension()
+ self.vector_store = QdrantConnectionManager.get_instance(**qdrant_config)
+ logger.info("✅ Qdrant向量数据库初始化完成")
+
+ # 初始化Neo4j图数据库
+ from ..storage.neo4j_store import Neo4jGraphStore
+ neo4j_config = db_config.get_neo4j_config()
+ self.graph_store = Neo4jGraphStore(**neo4j_config)
+ logger.info("✅ Neo4j图数据库初始化完成")
+
+ # 验证连接
+ vector_health = self.vector_store.health_check()
+ graph_health = self.graph_store.health_check()
+
+ if not vector_health:
+ logger.warning("⚠️ Qdrant连接异常,部分功能可能受限")
+ if not graph_health:
+ logger.warning("⚠️ Neo4j连接异常,图搜索功能可能受限")
+
+ logger.info(f"🏥 数据库健康状态: Qdrant={'✅' if vector_health else '❌'}, Neo4j={'✅' if graph_health else '❌'}")
+
+ except Exception as e:
+ logger.error(f"❌ 数据库初始化失败: {e}")
+ logger.info("💡 请检查数据库配置和网络连接")
+ logger.info("💡 参考 DATABASE_SETUP_GUIDE.md 进行配置")
+ raise
+
+ def _init_nlp(self):
+ """初始化NLP处理器 - 智能多语言支持"""
+ try:
+ import spacy
+ self.nlp_models = {}
+
+ # 尝试加载多语言模型
+ models_to_try = [
+ ("zh_core_web_sm", "中文"),
+ ("en_core_web_sm", "英文")
+ ]
+
+ loaded_models = []
+ for model_name, lang_name in models_to_try:
+ try:
+ nlp = spacy.load(model_name)
+ self.nlp_models[model_name] = nlp
+ loaded_models.append(lang_name)
+ logger.info(f"✅ 加载{lang_name}spaCy模型: {model_name}")
+ except OSError:
+ logger.warning(f"⚠️ {lang_name}spaCy模型不可用: {model_name}")
+
+ # 设置主要NLP处理器
+ if "zh_core_web_sm" in self.nlp_models:
+ self.nlp = self.nlp_models["zh_core_web_sm"]
+ logger.info("🎯 主要使用中文spaCy模型")
+ elif "en_core_web_sm" in self.nlp_models:
+ self.nlp = self.nlp_models["en_core_web_sm"]
+ logger.info("🎯 主要使用英文spaCy模型")
+ else:
+ self.nlp = None
+ logger.warning("⚠️ 无可用spaCy模型,实体提取将受限")
+
+ if loaded_models:
+ logger.info(f"📚 可用语言模型: {', '.join(loaded_models)}")
+
+ except ImportError:
+ logger.warning("⚠️ spaCy不可用,实体提取将受限")
+ self.nlp = None
+ self.nlp_models = {}
+
+ def add(self, memory_item: MemoryItem) -> str:
+ """添加语义记忆"""
+ try:
+ # 1. 生成文本嵌入
+ embedding = self.embedding_model.encode(memory_item.content)
+ self.memory_embeddings[memory_item.id] = embedding
+
+ # 2. 提取实体和关系
+ entities = self._extract_entities(memory_item.content)
+ relations = self._extract_relations(memory_item.content, entities)
+
+ # 3. 存储到Neo4j图数据库
+ for entity in entities:
+ self._add_entity_to_graph(entity, memory_item)
+
+ for relation in relations:
+ self._add_relation_to_graph(relation, memory_item)
+
+ # 4. 存储到Qdrant向量数据库
+ metadata = {
+ "memory_id": memory_item.id,
+ "user_id": memory_item.user_id,
+ "content": memory_item.content,
+ "memory_type": memory_item.memory_type,
+ "timestamp": int(memory_item.timestamp.timestamp()),
+ "importance": memory_item.importance,
+ "entities": [e.entity_id for e in entities],
+ "entity_count": len(entities),
+ "relation_count": len(relations)
+ }
+
+ success = self.vector_store.add_vectors(
+ vectors=[embedding.tolist()],
+ metadata=[metadata],
+ ids=[memory_item.id]
+ )
+
+ if not success:
+ logger.warning("⚠️ 向量存储失败,但记忆已添加到图数据库")
+
+ # 5. 添加实体信息到元数据
+ memory_item.metadata["entities"] = [e.entity_id for e in entities]
+ memory_item.metadata["relations"] = [
+ f"{r.from_entity}-{r.relation_type}-{r.to_entity}" for r in relations
+ ]
+
+ # 6. 存储记忆
+ self.semantic_memories.append(memory_item)
+
+ logger.info(f"✅ 添加语义记忆: {len(entities)}个实体, {len(relations)}个关系")
+ return memory_item.id
+
+ except Exception as e:
+ logger.error(f"❌ 添加语义记忆失败: {e}")
+ raise
+
+ def retrieve(self, query: str, limit: int = 5, **kwargs) -> List[MemoryItem]:
+ """检索语义记忆"""
+ try:
+ user_id = kwargs.get("user_id")
+
+ # 1. 向量检索
+ vector_results = self._vector_search(query, limit * 2, user_id)
+
+ # 2. 图检索
+ graph_results = self._graph_search(query, limit * 2, user_id)
+
+ # 3. 混合排序
+ combined_results = self._combine_and_rank_results(
+ vector_results, graph_results, query, limit
+ )
+
+ # 3.1 计算概率(对 combined_score 做 softmax 归一化)
+ scores = [r.get("combined_score", r.get("vector_score", 0.0)) for r in combined_results]
+ if scores:
+ import math
+ max_s = max(scores)
+ exps = [math.exp(s - max_s) for s in scores]
+ denom = sum(exps) or 1.0
+ probs = [e / denom for e in exps]
+ else:
+ probs = []
+
+ # 4. 过滤已遗忘记忆并转换为MemoryItem
+ result_memories = []
+ for idx, result in enumerate(combined_results):
+ memory_id = result.get("memory_id")
+
+ # 检查是否已遗忘
+ memory = next((m for m in self.semantic_memories if m.id == memory_id), None)
+ if memory and memory.metadata.get("forgotten", False):
+ continue # 跳过已遗忘的记忆
+
+ # 处理时间戳
+ timestamp = result.get("timestamp")
+ if isinstance(timestamp, str):
+ try:
+ timestamp = datetime.fromisoformat(timestamp)
+ except ValueError:
+ timestamp = datetime.now()
+ elif isinstance(timestamp, (int, float)):
+ timestamp = datetime.fromtimestamp(timestamp)
+ else:
+ timestamp = datetime.now()
+
+ # 直接从结果数据构建MemoryItem(附带分数与概率)
+ memory_item = MemoryItem(
+ id=result["memory_id"],
+ content=result["content"],
+ memory_type="semantic",
+ user_id=result.get("user_id", "default"),
+ timestamp=timestamp,
+ importance=result.get("importance", 0.5),
+ metadata={
+ **result.get("metadata", {}),
+ "combined_score": result.get("combined_score", 0.0),
+ "vector_score": result.get("vector_score", 0.0),
+ "graph_score": result.get("graph_score", 0.0),
+ "probability": probs[idx] if idx < len(probs) else 0.0,
+ }
+ )
+ result_memories.append(memory_item)
+
+ logger.info(f"✅ 检索到 {len(result_memories)} 条相关记忆")
+ return result_memories[:limit]
+
+ except Exception as e:
+ logger.error(f"❌ 检索语义记忆失败: {e}")
+ return []
+
+ def _vector_search(self, query: str, limit: int, user_id: Optional[str] = None) -> List[Dict[str, Any]]:
+ """Qdrant向量搜索"""
+ try:
+ # 生成查询向量
+ query_embedding = self.embedding_model.encode(query)
+
+ # 构建过滤条件
+ where_filter = {"memory_type": "semantic"}
+ if user_id:
+ where_filter["user_id"] = user_id
+
+ # Qdrant向量检索
+ results = self.vector_store.search_similar(
+ query_vector=query_embedding.tolist(),
+ limit=limit,
+ where=where_filter if where_filter else None
+ )
+
+ # 转换结果格式以保持兼容性
+ formatted_results = []
+ for result in results:
+ formatted_result = {
+ "id": result["id"],
+ "score": result["score"],
+ **result["metadata"] # 包含所有元数据
+ }
+ formatted_results.append(formatted_result)
+
+ logger.debug(f"🔍 Qdrant向量搜索返回 {len(formatted_results)} 个结果")
+ return formatted_results
+
+ except Exception as e:
+ logger.error(f"❌ Qdrant向量搜索失败: {e}")
+ return []
+
+ def _graph_search(self, query: str, limit: int, user_id: Optional[str] = None) -> List[Dict[str, Any]]:
+ """Neo4j图搜索"""
+ try:
+ # 从查询中提取实体
+ query_entities = self._extract_entities(query)
+
+ if not query_entities:
+ # 如果没有提取到实体,尝试按名称搜索
+ entities_by_name = self.graph_store.search_entities_by_name(
+ name_pattern=query,
+ limit=10
+ )
+ if entities_by_name:
+ query_entities = [Entity(
+ entity_id=e["id"],
+ name=e["name"],
+ entity_type=e["type"]
+ ) for e in entities_by_name[:3]]
+ else:
+ return []
+
+ # 在Neo4j图中查找相关实体和记忆
+ related_memory_ids = set()
+
+ for entity in query_entities:
+ try:
+ # 查找相关实体
+ related_entities = self.graph_store.find_related_entities(
+ entity_id=entity.entity_id,
+ max_depth=2,
+ limit=20
+ )
+
+ # 收集相关记忆ID
+ for rel_entity in related_entities:
+ if "memory_id" in rel_entity:
+ related_memory_ids.add(rel_entity["memory_id"])
+
+ # 也添加直接匹配的实体记忆
+ entity_rels = self.graph_store.get_entity_relationships(entity.entity_id)
+ for rel in entity_rels:
+ rel_data = rel.get("relationship", {})
+ if "memory_id" in rel_data:
+ related_memory_ids.add(rel_data["memory_id"])
+
+ except Exception as e:
+ logger.debug(f"图搜索实体 {entity.entity_id} 失败: {e}")
+ continue
+
+ # 构建结果 - 从向量数据库获取完整记忆信息
+ results = []
+ for memory_id in list(related_memory_ids)[:limit * 2]: # 获取更多候选
+ try:
+ # 优先从本地缓存获取记忆详情,避免占位向量维度不一致问题
+ mem = self._find_memory_by_id(memory_id)
+ if not mem:
+ continue
+
+ if user_id and mem.user_id != user_id:
+ continue
+
+ metadata = {
+ "content": mem.content,
+ "user_id": mem.user_id,
+ "memory_type": mem.memory_type,
+ "importance": mem.importance,
+ "timestamp": int(mem.timestamp.timestamp()),
+ "entities": mem.metadata.get("entities", [])
+ }
+
+ # 计算图相关性分数
+ graph_score = self._calculate_graph_relevance_neo4j(metadata, query_entities)
+
+ results.append({
+ "id": memory_id,
+ "memory_id": memory_id,
+ "content": metadata.get("content", ""),
+ "similarity": graph_score,
+ "user_id": metadata.get("user_id"),
+ "memory_type": metadata.get("memory_type"),
+ "importance": metadata.get("importance", 0.5),
+ "timestamp": metadata.get("timestamp"),
+ "entities": metadata.get("entities", [])
+ })
+
+ except Exception as e:
+ logger.debug(f"获取记忆 {memory_id} 详情失败: {e}")
+ continue
+
+ # 按图相关性排序
+ results.sort(key=lambda x: x["similarity"], reverse=True)
+ logger.debug(f"🕸️ Neo4j图搜索返回 {len(results)} 个结果")
+ return results[:limit]
+
+ except Exception as e:
+ logger.error(f"❌ Neo4j图搜索失败: {e}")
+ return []
+
+ def _combine_and_rank_results(
+ self,
+ vector_results: List[Dict[str, Any]],
+ graph_results: List[Dict[str, Any]],
+ query: str,
+ limit: int
+ ) -> List[Dict[str, Any]]:
+ """混合排序结果 - 仅基于向量与图分数的简单融合"""
+ # 合并结果,按内容去重
+ combined = {}
+ content_seen = set() # 用于内容去重
+
+ # 添加向量结果
+ for result in vector_results:
+ memory_id = result["memory_id"]
+ content = result.get("content", "")
+
+ # 内容去重:检查是否已经有相同或高度相似的内容
+ content_hash = hash(content.strip())
+ if content_hash in content_seen:
+ logger.debug(f"⚠️ 跳过重复内容: {content[:30]}...")
+ continue
+
+ content_seen.add(content_hash)
+ combined[memory_id] = {
+ **result,
+ "vector_score": result.get("score", 0.0),
+ "graph_score": 0.0,
+ "content_hash": content_hash
+ }
+
+ # 添加图结果
+ for result in graph_results:
+ memory_id = result["memory_id"]
+ content = result.get("content", "")
+ content_hash = hash(content.strip())
+
+ if memory_id in combined:
+ combined[memory_id]["graph_score"] = result.get("similarity", 0.0)
+ elif content_hash not in content_seen:
+ content_seen.add(content_hash)
+ combined[memory_id] = {
+ **result,
+ "vector_score": 0.0,
+ "graph_score": result.get("similarity", 0.0),
+ "content_hash": content_hash
+ }
+
+ # 计算混合分数:相似度为主,重要性为辅助排序因子
+ for memory_id, result in combined.items():
+ vector_score = result["vector_score"]
+ graph_score = result["graph_score"]
+ importance = result.get("importance", 0.5)
+
+ # 新评分算法:向量检索纯基于相似度,重要性作为加权因子
+ # 基础相似度得分(不受重要性影响)
+ base_relevance = vector_score * 0.7 + graph_score * 0.3
+
+ # 重要性作为乘法加权因子,范围 [0.8, 1.2]
+ # importance in [0,1] -> weight in [0.8,1.2]
+ importance_weight = 0.8 + (importance * 0.4)
+
+ # 最终得分:相似度 * 重要性权重
+ combined_score = base_relevance * importance_weight
+
+ # 调试信息:查看分数分解
+ result["debug_info"] = {
+ "base_relevance": base_relevance,
+ "importance_weight": importance_weight,
+ "combined_score": combined_score
+ }
+
+ result["combined_score"] = combined_score
+
+ # 应用最小相关性阈值
+ min_threshold = 0.1 # 最小相关性阈值
+ filtered_results = [
+ result for result in combined.values()
+ if result["combined_score"] >= min_threshold
+ ]
+
+ # 排序并返回
+ sorted_results = sorted(
+ filtered_results,
+ key=lambda x: x["combined_score"],
+ reverse=True
+ )
+
+ # 调试信息
+ logger.debug(f"🔍 向量结果: {len(vector_results)}, 图结果: {len(graph_results)}")
+ logger.debug(f"📝 去重后: {len(combined)}, 过滤后: {len(filtered_results)}")
+
+ if logger.level <= logging.DEBUG:
+ for i, result in enumerate(sorted_results[:3]):
+ logger.debug(f" 结果{i+1}: 向量={result['vector_score']:.3f}, 图={result['graph_score']:.3f}, 精确={result.get('exact_match_bonus', 0):.3f}, 关键词={result.get('keyword_bonus', 0):.3f}, 公司={result.get('company_bonus', 0):.3f}, 实体={result.get('entity_type_bonus', 0):.3f}, 综合={result['combined_score']:.3f}")
+
+ return sorted_results[:limit]
+
+ def _detect_language(self, text: str) -> str:
+ """简单的语言检测"""
+ # 统计中文字符比例(无正则,逐字符判断范围)
+ chinese_chars = sum(1 for ch in text if '\u4e00' <= ch <= '\u9fff')
+ total_chars = len(text.replace(' ', ''))
+
+ if total_chars == 0:
+ return "en"
+
+ chinese_ratio = chinese_chars / total_chars
+ return "zh" if chinese_ratio > 0.3 else "en"
+
+ def _extract_entities(self, text: str) -> List[Entity]:
+ """智能多语言实体提取"""
+ entities = []
+
+ # 检测文本语言
+ lang = self._detect_language(text)
+
+ # 选择合适的spaCy模型
+ selected_nlp = None
+ if lang == "zh" and "zh_core_web_sm" in self.nlp_models:
+ selected_nlp = self.nlp_models["zh_core_web_sm"]
+ elif lang == "en" and "en_core_web_sm" in self.nlp_models:
+ selected_nlp = self.nlp_models["en_core_web_sm"]
+ else:
+ # 使用默认模型
+ selected_nlp = self.nlp
+
+ logger.debug(f"🌐 检测语言: {lang}, 使用模型: {selected_nlp.meta['name'] if selected_nlp else 'None'}")
+
+ # 使用spaCy进行实体识别和词法分析
+ if selected_nlp:
+ try:
+ doc = selected_nlp(text)
+ logger.debug(f"📝 spaCy处理文本: '{text}' -> {len(doc.ents)} 个实体")
+
+ # 存储词法分析结果,供Neo4j使用
+ self._store_linguistic_analysis(doc, text)
+
+ if not doc.ents:
+ # 如果没有实体,记录详细的词元信息
+ logger.debug("🔍 未找到实体,词元分析:")
+ for token in doc[:5]: # 只显示前5个词元
+ logger.debug(f" '{token.text}' -> POS: {token.pos_}, TAG: {token.tag_}, ENT_IOB: {token.ent_iob_}")
+
+ for ent in doc.ents:
+ entity = Entity(
+ entity_id=f"entity_{hash(ent.text)}",
+ name=ent.text,
+ entity_type=ent.label_,
+ description=f"从文本中识别的{ent.label_}实体"
+ )
+ entities.append(entity)
+ # 安全获取置信度信息
+ confidence = "N/A"
+ try:
+ if hasattr(ent._, 'confidence'):
+ confidence = getattr(ent._, 'confidence', 'N/A')
+ except:
+ confidence = "N/A"
+
+ logger.debug(f"🏷️ spaCy识别实体: '{ent.text}' -> {ent.label_} (置信度: {confidence})")
+
+ except Exception as e:
+ logger.warning(f"⚠️ spaCy实体识别失败: {e}")
+ import traceback
+ logger.debug(f"详细错误: {traceback.format_exc()}")
+ else:
+ logger.warning("⚠️ 没有可用的spaCy模型进行实体识别")
+
+ return entities
+
+ def _store_linguistic_analysis(self, doc, text: str):
+ """存储spaCy词法分析结果到Neo4j"""
+ if not self.graph_store:
+ return
+
+ try:
+ # 为每个词元创建节点
+ for token in doc:
+ # 跳过标点符号和空格
+ if token.is_punct or token.is_space:
+ continue
+
+ token_id = f"token_{hash(token.text + token.pos_)}"
+
+ # 添加词元节点到Neo4j
+ self.graph_store.add_entity(
+ entity_id=token_id,
+ name=token.text,
+ entity_type="TOKEN",
+ properties={
+ "pos": token.pos_, # 词性(NOUN, VERB等)
+ "tag": token.tag_, # 细粒度标签
+ "lemma": token.lemma_, # 词元原形
+ "is_alpha": token.is_alpha,
+ "is_stop": token.is_stop,
+ "source_text": text[:50], # 来源文本片段
+ "language": self._detect_language(text)
+ }
+ )
+
+ # 如果是名词,可能是潜在的概念
+ if token.pos_ in ["NOUN", "PROPN"]:
+ concept_id = f"concept_{hash(token.text)}"
+ self.graph_store.add_entity(
+ entity_id=concept_id,
+ name=token.text,
+ entity_type="CONCEPT",
+ properties={
+ "category": token.pos_,
+ "frequency": 1, # 可以后续累计
+ "source_text": text[:50]
+ }
+ )
+
+ # 建立词元到概念的关系
+ self.graph_store.add_relationship(
+ from_entity_id=token_id,
+ to_entity_id=concept_id,
+ relationship_type="REPRESENTS",
+ properties={"confidence": 1.0}
+ )
+
+ # 建立词元之间的依存关系
+ for token in doc:
+ if token.is_punct or token.is_space or token.head == token:
+ continue
+
+ from_id = f"token_{hash(token.text + token.pos_)}"
+ to_id = f"token_{hash(token.head.text + token.head.pos_)}"
+
+ # Neo4j不允许关系类型包含冒号,需要清理
+ relation_type = token.dep_.upper().replace(":", "_")
+
+ self.graph_store.add_relationship(
+ from_entity_id=from_id,
+ to_entity_id=to_id,
+ relationship_type=relation_type, # 清理后的依存关系类型
+ properties={
+ "dependency": token.dep_, # 保留原始依存关系
+ "source_text": text[:50]
+ }
+ )
+
+ logger.debug(f"🔗 已将词法分析结果存储到Neo4j: {len([t for t in doc if not t.is_punct and not t.is_space])} 个词元")
+
+ except Exception as e:
+ logger.warning(f"⚠️ 存储词法分析失败: {e}")
+
+ def _extract_relations(self, text: str, entities: List[Entity]) -> List[Relation]:
+ """提取关系"""
+ relations = []
+ # 仅保留简单共现关系,不做任何正则/关键词匹配
+ for i, entity1 in enumerate(entities):
+ for entity2 in entities[i+1:]:
+ relations.append(Relation(
+ from_entity=entity1.entity_id,
+ to_entity=entity2.entity_id,
+ relation_type="CO_OCCURS",
+ strength=0.5,
+ evidence=text[:100]
+ ))
+ return relations
+
+ def _add_entity_to_graph(self, entity: Entity, memory_item: MemoryItem):
+ """添加实体到Neo4j图数据库"""
+ try:
+ # 准备实体属性
+ properties = {
+ "name": entity.name,
+ "description": entity.description,
+ "frequency": entity.frequency,
+ "memory_id": memory_item.id,
+ "user_id": memory_item.user_id,
+ "importance": memory_item.importance,
+ **entity.properties
+ }
+
+ # 添加到Neo4j
+ success = self.graph_store.add_entity(
+ entity_id=entity.entity_id,
+ name=entity.name,
+ entity_type=entity.entity_type,
+ properties=properties
+ )
+
+ if success:
+ # 同时更新本地缓存
+ if entity.entity_id in self.entities:
+ self.entities[entity.entity_id].frequency += 1
+ self.entities[entity.entity_id].updated_at = datetime.now()
+ else:
+ self.entities[entity.entity_id] = entity
+
+ return success
+
+ except Exception as e:
+ logger.error(f"❌ 添加实体到图数据库失败: {e}")
+ return False
+
+ def _add_relation_to_graph(self, relation: Relation, memory_item: MemoryItem):
+ """添加关系到Neo4j图数据库"""
+ try:
+ # 准备关系属性
+ properties = {
+ "strength": relation.strength,
+ "memory_id": memory_item.id,
+ "user_id": memory_item.user_id,
+ "importance": memory_item.importance,
+ "evidence": relation.evidence
+ }
+
+ # 添加到Neo4j
+ success = self.graph_store.add_relationship(
+ from_entity_id=relation.from_entity,
+ to_entity_id=relation.to_entity,
+ relationship_type=relation.relation_type,
+ properties=properties
+ )
+
+ if success:
+ # 同时更新本地缓存
+ self.relations.append(relation)
+
+ return success
+
+ except Exception as e:
+ logger.error(f"❌ 添加关系到图数据库失败: {e}")
+ return False
+
+ def _calculate_graph_relevance_neo4j(self, memory_metadata: Dict[str, Any], query_entities: List[Entity]) -> float:
+ """计算Neo4j图相关性分数"""
+ try:
+ memory_entities = memory_metadata.get("entities", [])
+ if not memory_entities or not query_entities:
+ return 0.0
+
+ # 实体匹配度
+ query_entity_ids = {e.entity_id for e in query_entities}
+ matching_entities = len(set(memory_entities).intersection(query_entity_ids))
+ entity_score = matching_entities / len(query_entity_ids) if query_entity_ids else 0
+
+ # 实体数量加权
+ entity_count = memory_metadata.get("entity_count", 0)
+ entity_density = min(entity_count / 10, 1.0) # 归一化到[0,1]
+
+ # 关系数量加权
+ relation_count = memory_metadata.get("relation_count", 0)
+ relation_density = min(relation_count / 5, 1.0) # 归一化到[0,1]
+
+ # 综合分数
+ relevance_score = (
+ entity_score * 0.6 + # 实体匹配权重60%
+ entity_density * 0.2 + # 实体密度权重20%
+ relation_density * 0.2 # 关系密度权重20%
+ )
+
+ return min(relevance_score, 1.0)
+
+ except Exception as e:
+ logger.debug(f"计算图相关性失败: {e}")
+ return 0.0
+
+ def _add_or_update_entity(self, entity: Entity):
+ """添加或更新实体"""
+ if entity.entity_id in self.entities:
+ # 更新现有实体
+ existing = self.entities[entity.entity_id]
+ existing.frequency += 1
+ existing.updated_at = datetime.now()
+ else:
+ # 添加新实体
+ self.entities[entity.entity_id] = entity
+
+ def _add_or_update_relation(self, relation: Relation):
+ """添加或更新关系"""
+ # 检查是否已存在相同关系
+ existing_relation = None
+ for r in self.relations:
+ if (r.from_entity == relation.from_entity and
+ r.to_entity == relation.to_entity and
+ r.relation_type == relation.relation_type):
+ existing_relation = r
+ break
+
+ if existing_relation:
+ # 更新现有关系
+ existing_relation.frequency += 1
+ existing_relation.strength = min(1.0, existing_relation.strength + 0.1)
+ else:
+ # 添加新关系
+ self.relations.append(relation)
+
+ # 旧的图相关性计算方法已被 _calculate_graph_relevance_neo4j 替代
+
+ def _find_memory_by_id(self, memory_id: str) -> Optional[MemoryItem]:
+ """根据ID查找记忆"""
+ logger.debug(f"🔍 查找记忆ID: {memory_id}, 当前记忆数: {len(self.semantic_memories)}")
+ for memory in self.semantic_memories:
+ if memory.id == memory_id:
+ logger.debug(f"✅ 找到记忆: {memory.content[:50]}...")
+ return memory
+ logger.debug(f"❌ 未找到记忆ID: {memory_id}")
+ return None
+
+ def update(
+ self,
+ memory_id: str,
+ content: str = None,
+ importance: float = None,
+ metadata: Dict[str, Any] = None
+ ) -> bool:
+ """更新语义记忆"""
+ memory = self._find_memory_by_id(memory_id)
+ if not memory:
+ return False
+
+ try:
+ if content is not None:
+ # 重新生成嵌入和提取实体
+ embedding = self.embedding_model.encode(content)
+ self.memory_embeddings[memory_id] = embedding
+
+ # 清理旧的实体关系
+ old_entities = memory.metadata.get("entities", [])
+ self._cleanup_entities_and_relations(old_entities)
+
+ # 提取新的实体和关系
+ memory.content = content
+ entities = self._extract_entities(content)
+ relations = self._extract_relations(content, entities)
+
+ # 更新知识图谱
+ for entity in entities:
+ self._add_or_update_entity(entity)
+ for relation in relations:
+ self._add_or_update_relation(relation)
+
+ # 更新元数据
+ memory.metadata["entities"] = [e.entity_id for e in entities]
+ memory.metadata["relations"] = [
+ f"{r.from_entity}-{r.relation_type}-{r.to_entity}" for r in relations
+ ]
+
+ if importance is not None:
+ memory.importance = importance
+
+ if metadata is not None:
+ memory.metadata.update(metadata)
+
+ return True
+
+ except Exception as e:
+ logger.error(f"❌ 更新记忆失败: {e}")
+ return False
+
+ def remove(self, memory_id: str) -> bool:
+ """删除语义记忆"""
+ memory = self._find_memory_by_id(memory_id)
+ if not memory:
+ return False
+
+ try:
+ # 删除向量
+ self.vector_store.delete_memories([memory_id])
+
+ # 清理实体和关系
+ entities = memory.metadata.get("entities", [])
+ self._cleanup_entities_and_relations(entities)
+
+ # 删除记忆
+ self.semantic_memories.remove(memory)
+ if memory_id in self.memory_embeddings:
+ del self.memory_embeddings[memory_id]
+
+ return True
+
+ except Exception as e:
+ logger.error(f"❌ 删除记忆失败: {e}")
+ return False
+
+ def _cleanup_entities_and_relations(self, entity_ids: List[str]):
+ """清理实体和关系"""
+ # 这里可以实现更智能的清理逻辑
+ # 例如,如果实体不再被任何记忆引用,则删除它
+ pass
+
+ def has_memory(self, memory_id: str) -> bool:
+ """检查记忆是否存在"""
+ return self._find_memory_by_id(memory_id) is not None
+
+ def forget(self, strategy: str = "importance_based", threshold: float = 0.1, max_age_days: int = 30) -> int:
+ """语义记忆遗忘机制(硬删除)"""
+ forgotten_count = 0
+ current_time = datetime.now()
+
+ to_remove = [] # 收集要删除的记忆ID
+
+ for memory in self.semantic_memories:
+ should_forget = False
+
+ if strategy == "importance_based":
+ # 基于重要性遗忘
+ if memory.importance < threshold:
+ should_forget = True
+ elif strategy == "time_based":
+ # 基于时间遗忘
+ cutoff_time = current_time - timedelta(days=max_age_days)
+ if memory.timestamp < cutoff_time:
+ should_forget = True
+ elif strategy == "capacity_based":
+ # 基于容量遗忘(保留最重要的)
+ if len(self.semantic_memories) > self.config.max_capacity:
+ sorted_memories = sorted(self.semantic_memories, key=lambda m: m.importance)
+ excess_count = len(self.semantic_memories) - self.config.max_capacity
+ if memory in sorted_memories[:excess_count]:
+ should_forget = True
+
+ if should_forget:
+ to_remove.append(memory.id)
+
+ # 执行硬删除
+ for memory_id in to_remove:
+ if self.remove(memory_id):
+ forgotten_count += 1
+ logger.info(f"语义记忆硬删除: {memory_id[:8]}... (策略: {strategy})")
+
+ return forgotten_count
+
+ def clear(self):
+ """清空所有语义记忆 - 包括专业数据库"""
+ try:
+ # 清空Qdrant向量数据库
+ if self.vector_store:
+ success = self.vector_store.clear_collection()
+ if success:
+ logger.info("✅ Qdrant向量数据库已清空")
+ else:
+ logger.warning("⚠️ Qdrant清空失败")
+
+ # 清空Neo4j图数据库
+ if self.graph_store:
+ success = self.graph_store.clear_all()
+ if success:
+ logger.info("✅ Neo4j图数据库已清空")
+ else:
+ logger.warning("⚠️ Neo4j清空失败")
+
+ # 清空本地缓存
+ self.semantic_memories.clear()
+ self.memory_embeddings.clear()
+ self.entities.clear()
+ self.relations.clear()
+
+ logger.info("🧹 语义记忆系统已完全清空")
+
+ except Exception as e:
+ logger.error(f"❌ 清空语义记忆失败: {e}")
+ # 即使数据库清空失败,也要清空本地缓存
+ self.semantic_memories.clear()
+ self.memory_embeddings.clear()
+ self.entities.clear()
+ self.relations.clear()
+
+ def get_all(self) -> List[MemoryItem]:
+ """获取所有语义记忆"""
+ return self.semantic_memories.copy()
+
+ def get_stats(self) -> Dict[str, Any]:
+ """获取语义记忆统计信息"""
+ graph_stats = {}
+ try:
+ if self.graph_store:
+ graph_stats = self.graph_store.get_stats() or {}
+ except Exception:
+ graph_stats = {}
+
+ # 硬删除模式:所有记忆都是活跃的
+ active_memories = self.semantic_memories
+
+ return {
+ "count": len(active_memories), # 活跃记忆数量
+ "forgotten_count": 0, # 硬删除模式下已遗忘的记忆会被直接删除
+ "total_count": len(self.semantic_memories), # 总记忆数量
+ "entities_count": len(self.entities),
+ "relations_count": len(self.relations),
+ "graph_nodes": graph_stats.get("total_nodes", 0),
+ "graph_edges": graph_stats.get("total_relationships", 0),
+ "avg_importance": sum(m.importance for m in active_memories) / len(active_memories) if active_memories else 0.0,
+ "memory_type": "enhanced_semantic"
+ }
+
+ def get_entity(self, entity_id: str) -> Optional[Entity]:
+ """获取实体"""
+ return self.entities.get(entity_id)
+
+ def search_entities(self, query: str, limit: int = 10) -> List[Entity]:
+ """搜索实体"""
+ query_lower = query.lower()
+ scored_entities = []
+
+ for entity in self.entities.values():
+ score = 0.0
+
+ # 名称匹配
+ if query_lower in entity.name.lower():
+ score += 2.0
+
+ # 类型匹配
+ if query_lower in entity.entity_type.lower():
+ score += 1.0
+
+ # 描述匹配
+ if query_lower in entity.description.lower():
+ score += 0.5
+
+ # 频率权重
+ score *= math.log(1 + entity.frequency)
+
+ if score > 0:
+ scored_entities.append((score, entity))
+
+ scored_entities.sort(key=lambda x: x[0], reverse=True)
+ return [entity for _, entity in scored_entities[:limit]]
+
+ def get_related_entities(
+ self,
+ entity_id: str,
+ relation_types: List[str] = None,
+ max_hops: int = 2
+ ) -> List[Dict[str, Any]]:
+ """获取相关实体 - 使用Neo4j图数据库"""
+
+ related = []
+
+ try:
+ # 使用Neo4j图数据库查找相关实体
+ if not self.graph_store:
+ logger.warning("⚠️ Neo4j图数据库不可用")
+ return []
+
+ # 使用Neo4j查找相关实体
+ related_entities = self.graph_store.find_related_entities(
+ entity_id=entity_id,
+ relationship_types=relation_types,
+ max_depth=max_hops,
+ limit=50
+ )
+
+ # 转换格式以保持兼容性
+ for entity_data in related_entities:
+ # 尝试从本地缓存获取实体对象
+ entity_obj = self.entities.get(entity_data.get("id"))
+ if not entity_obj:
+ # 如果本地缓存没有,创建临时实体对象
+ entity_obj = Entity(
+ entity_id=entity_data.get("id", entity_id),
+ name=entity_data.get("name", ""),
+ entity_type=entity_data.get("type", "MISC")
+ )
+
+ related.append({
+ "entity": entity_obj,
+ "relation_type": entity_data.get("relationship_path", ["RELATED"])[-1] if entity_data.get("relationship_path") else "RELATED",
+ "strength": 1.0 / max(entity_data.get("distance", 1), 1), # 距离越近强度越高
+ "distance": entity_data.get("distance", max_hops)
+ })
+
+ # 按距离和强度排序
+ related.sort(key=lambda x: (x["distance"], -x["strength"]))
+
+ except Exception as e:
+ logger.error(f"❌ 获取相关实体失败: {e}")
+
+ return related
+
+ def export_knowledge_graph(self) -> Dict[str, Any]:
+ """导出知识图谱 - 从Neo4j获取统计信息"""
+ try:
+ # 从Neo4j获取统计信息
+ stats = {}
+ if self.graph_store:
+ stats = self.graph_store.get_stats()
+
+ return {
+ "entities": {eid: entity.to_dict() for eid, entity in self.entities.items()},
+ "relations": [relation.to_dict() for relation in self.relations],
+ "graph_stats": {
+ "total_nodes": stats.get("total_nodes", 0),
+ "entity_nodes": stats.get("entity_nodes", 0),
+ "memory_nodes": stats.get("memory_nodes", 0),
+ "total_relationships": stats.get("total_relationships", 0),
+ "cached_entities": len(self.entities),
+ "cached_relations": len(self.relations)
+ }
+ }
+ except Exception as e:
+ logger.error(f"❌ 导出知识图谱失败: {e}")
+ return {
+ "entities": {},
+ "relations": [],
+ "graph_stats": {"error": str(e)}
+ }
diff --git a/Co-creation-projects/aug618-Praxis/memory/types/working.py b/Co-creation-projects/aug618-Praxis/memory/types/working.py
new file mode 100644
index 00000000..d323569c
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/memory/types/working.py
@@ -0,0 +1,412 @@
+"""工作记忆实现
+
+按照第8章架构设计的工作记忆,提供:
+- 短期上下文管理
+- 容量和时间限制
+- 优先级管理
+- 自动清理机制
+"""
+
+from typing import List, Dict, Any
+from datetime import datetime, timedelta
+import heapq
+
+from ..base import BaseMemory, MemoryItem, MemoryConfig
+
+class WorkingMemory(BaseMemory):
+ """工作记忆实现
+
+ 特点:
+ - 容量有限(通常10-20条记忆)
+ - 时效性强(会话级别)
+ - 优先级管理
+ - 自动清理过期记忆
+ """
+
+ def __init__(self, config: MemoryConfig, storage_backend=None):
+ super().__init__(config, storage_backend)
+
+ # 工作记忆特定配置
+ self.max_capacity = self.config.working_memory_capacity
+ self.max_tokens = self.config.working_memory_tokens
+ # 纯内存TTL(分钟),可通过在 MemoryConfig 上挂载 working_memory_ttl_minutes 覆盖
+ self.max_age_minutes = getattr(self.config, 'working_memory_ttl_minutes', 120)
+ self.current_tokens = 0
+ self.session_start = datetime.now()
+
+ # 内存存储(工作记忆不需要持久化)
+ self.memories: List[MemoryItem] = []
+
+ # 使用优先级队列管理记忆
+ self.memory_heap = [] # (priority, timestamp, memory_item)
+
+ def add(self, memory_item: MemoryItem) -> str:
+ """添加工作记忆"""
+ # 过期清理
+ self._expire_old_memories()
+ # 计算优先级(重要性 + 时间衰减)
+ priority = self._calculate_priority(memory_item)
+
+ # 添加到堆中
+ heapq.heappush(self.memory_heap, (-priority, memory_item.timestamp, memory_item))
+ self.memories.append(memory_item)
+
+ # 更新token计数
+ self.current_tokens += len(memory_item.content.split())
+
+ # 检查容量限制
+ self._enforce_capacity_limits()
+
+ return memory_item.id
+
+ def retrieve(self, query: str, limit: int = 5, user_id: str = None, **kwargs) -> List[MemoryItem]:
+ """检索工作记忆 - 混合语义向量检索和关键词匹配"""
+ # 过期清理
+ self._expire_old_memories()
+ if not self.memories:
+ return []
+
+ # 过滤已遗忘的记忆
+ active_memories = [m for m in self.memories if not m.metadata.get("forgotten", False)]
+
+ # 按用户ID过滤(如果提供)
+ filtered_memories = active_memories
+ if user_id:
+ filtered_memories = [m for m in active_memories if m.user_id == user_id]
+
+ if not filtered_memories:
+ return []
+
+ # 尝试语义向量检索(如果有嵌入模型)
+ vector_scores = {}
+ try:
+ # 简单的语义相似度计算(使用TF-IDF或其他轻量级方法)
+ from sklearn.feature_extraction.text import TfidfVectorizer
+ from sklearn.metrics.pairwise import cosine_similarity
+ import numpy as np
+
+ # 准备文档
+ documents = [query] + [m.content for m in filtered_memories]
+
+ # TF-IDF向量化
+ vectorizer = TfidfVectorizer(stop_words=None, lowercase=True)
+ tfidf_matrix = vectorizer.fit_transform(documents)
+
+ # 计算相似度
+ query_vector = tfidf_matrix[0:1]
+ doc_vectors = tfidf_matrix[1:]
+ similarities = cosine_similarity(query_vector, doc_vectors).flatten()
+
+ # 存储向量分数
+ for i, memory in enumerate(filtered_memories):
+ vector_scores[memory.id] = similarities[i]
+
+ except Exception as e:
+ # 如果向量检索失败,回退到关键词匹配
+ vector_scores = {}
+
+ # 计算最终分数
+ query_lower = query.lower()
+ scored_memories = []
+
+ for memory in filtered_memories:
+ content_lower = memory.content.lower()
+
+ # 获取向量分数(如果有)
+ vector_score = vector_scores.get(memory.id, 0.0)
+
+ # 关键词匹配分数
+ keyword_score = 0.0
+ if query_lower in content_lower:
+ keyword_score = len(query_lower) / len(content_lower)
+ else:
+ # 分词匹配
+ query_words = set(query_lower.split())
+ content_words = set(content_lower.split())
+ #按交集计算分数
+ intersection = query_words.intersection(content_words)
+ if intersection:
+ keyword_score = len(intersection) / len(query_words.union(content_words)) * 0.8
+
+ # 混合分数:向量检索 + 关键词匹配
+ if vector_score > 0:
+ base_relevance = vector_score * 0.7 + keyword_score * 0.3
+ else:
+ base_relevance = keyword_score
+
+ # 时间衰减
+ time_decay = self._calculate_time_decay(memory.timestamp)
+ base_relevance *= time_decay
+
+ # 重要性权重
+ importance_weight = 0.8 + (memory.importance * 0.4)
+ final_score = base_relevance * importance_weight
+
+ if final_score > 0:
+ scored_memories.append((final_score, memory))
+
+ # 按分数排序并返回
+ scored_memories.sort(key=lambda x: x[0], reverse=True)
+ return [memory for _, memory in scored_memories[:limit]]
+
+ def update(
+ self,
+ memory_id: str,
+ content: str = None,
+ importance: float = None,
+ metadata: Dict[str, Any] = None
+ ) -> bool:
+ """更新工作记忆"""
+ for memory in self.memories:
+ if memory.id == memory_id:
+ old_tokens = len(memory.content.split())
+
+ if content is not None:
+ memory.content = content
+ # 更新token计数
+ new_tokens = len(content.split())
+ self.current_tokens = self.current_tokens - old_tokens + new_tokens
+
+ if importance is not None:
+ memory.importance = importance
+
+ if metadata is not None:
+ memory.metadata.update(metadata)
+
+ # 重新计算优先级并更新堆
+ self._update_heap_priority(memory)
+
+ return True
+ return False
+
+ def remove(self, memory_id: str) -> bool:
+ """删除工作记忆"""
+ for i, memory in enumerate(self.memories):
+ if memory.id == memory_id:
+ # 从列表中删除
+ removed_memory = self.memories.pop(i)
+
+ # 从堆中删除(标记删除)
+ self._mark_deleted_in_heap(memory_id)
+
+ # 更新token计数
+ self.current_tokens -= len(removed_memory.content.split())
+ self.current_tokens = max(0, self.current_tokens)
+
+ return True
+ return False
+
+ def has_memory(self, memory_id: str) -> bool:
+ """检查记忆是否存在"""
+ return any(memory.id == memory_id for memory in self.memories)
+
+ def clear(self):
+ """清空所有工作记忆"""
+ self.memories.clear()
+ self.memory_heap.clear()
+ self.current_tokens = 0
+
+ def get_stats(self) -> Dict[str, Any]:
+ """获取工作记忆统计信息"""
+ # 过期清理(惰性)
+ self._expire_old_memories()
+
+ # 工作记忆中的记忆都是活跃的(已遗忘的记忆会被直接删除)
+ active_memories = self.memories
+
+ return {
+ "count": len(active_memories), # 活跃记忆数量
+ "forgotten_count": 0, # 工作记忆中已遗忘的记忆会被直接删除
+ "total_count": len(self.memories), # 总记忆数量
+ "current_tokens": self.current_tokens,
+ "max_capacity": self.max_capacity,
+ "max_tokens": self.max_tokens,
+ "max_age_minutes": self.max_age_minutes,
+ "session_duration_minutes": (datetime.now() - self.session_start).total_seconds() / 60,
+ "avg_importance": sum(m.importance for m in active_memories) / len(active_memories) if active_memories else 0.0,
+ "capacity_usage": len(active_memories) / self.max_capacity if self.max_capacity > 0 else 0.0,
+ "token_usage": self.current_tokens / self.max_tokens if self.max_tokens > 0 else 0.0,
+ "memory_type": "working"
+ }
+
+ def get_recent(self, limit: int = 10) -> List[MemoryItem]:
+ """获取最近的记忆"""
+ sorted_memories = sorted(
+ self.memories,
+ key=lambda x: x.timestamp,
+ reverse=True
+ )
+ return sorted_memories[:limit]
+
+ def get_important(self, limit: int = 10) -> List[MemoryItem]:
+ """获取重要记忆"""
+ sorted_memories = sorted(
+ self.memories,
+ key=lambda x: x.importance,
+ reverse=True
+ )
+ return sorted_memories[:limit]
+
+ def get_all(self) -> List[MemoryItem]:
+ """获取所有记忆"""
+ return self.memories.copy()
+
+ def get_context_summary(self, max_length: int = 500) -> str:
+ """获取上下文摘要"""
+ if not self.memories:
+ return "No working memories available."
+
+ # 按重要性和时间排序
+ sorted_memories = sorted(
+ self.memories,
+ key=lambda m: (m.importance, m.timestamp),
+ reverse=True
+ )
+
+ summary_parts = []
+ current_length = 0
+
+ for memory in sorted_memories:
+ content = memory.content
+ if current_length + len(content) <= max_length:
+ summary_parts.append(content)
+ current_length += len(content)
+ else:
+ # 截断最后一个记忆
+ remaining = max_length - current_length
+ if remaining > 50: # 至少保留50个字符
+ summary_parts.append(content[:remaining] + "...")
+ break
+
+ return "Working Memory Context:\n" + "\n".join(summary_parts)
+
+ def forget(self, strategy: str = "importance_based", threshold: float = 0.1, max_age_days: int = 1) -> int:
+ """工作记忆遗忘机制"""
+ forgotten_count = 0
+ current_time = datetime.now()
+
+ to_remove = []
+
+ # 始终先执行TTL过期(分钟级)
+ cutoff_ttl = current_time - timedelta(minutes=self.max_age_minutes)
+ for memory in self.memories:
+ if memory.timestamp < cutoff_ttl:
+ to_remove.append(memory.id)
+
+ if strategy == "importance_based":
+ # 删除低重要性记忆
+ for memory in self.memories:
+ if memory.importance < threshold:
+ to_remove.append(memory.id)
+
+ elif strategy == "time_based":
+ # 删除过期记忆(工作记忆通常以小时计算)
+ cutoff_time = current_time - timedelta(hours=max_age_days * 24)
+ for memory in self.memories:
+ if memory.timestamp < cutoff_time:
+ to_remove.append(memory.id)
+
+ elif strategy == "capacity_based":
+ # 删除超出容量的记忆
+ if len(self.memories) > self.max_capacity:
+ # 按优先级排序,删除最低的
+ sorted_memories = sorted(
+ self.memories,
+ key=lambda m: self._calculate_priority(m)
+ )
+ excess_count = len(self.memories) - self.max_capacity
+ for memory in sorted_memories[:excess_count]:
+ to_remove.append(memory.id)
+
+ # 执行删除
+ for memory_id in to_remove:
+ if self.remove(memory_id):
+ forgotten_count += 1
+
+ return forgotten_count
+
+ def _calculate_priority(self, memory: MemoryItem) -> float:
+ """计算记忆优先级"""
+ # 基础优先级 = 重要性
+ priority = memory.importance
+
+ # 时间衰减
+ time_decay = self._calculate_time_decay(memory.timestamp)
+ priority *= time_decay
+
+ return priority
+
+ def _calculate_time_decay(self, timestamp: datetime) -> float:
+ """计算时间衰减因子"""
+ time_diff = datetime.now() - timestamp
+ hours_passed = time_diff.total_seconds() / 3600
+
+ # 指数衰减(工作记忆衰减更快)
+ decay_factor = self.config.decay_factor ** (hours_passed / 6) # 每6小时衰减
+ return max(0.1, decay_factor) # 最小保持10%的权重
+
+ def _enforce_capacity_limits(self):
+ """强制执行容量限制"""
+ # 检查记忆数量限制
+ while len(self.memories) > self.max_capacity:
+ self._remove_lowest_priority_memory()
+
+ # 检查token限制
+ while self.current_tokens > self.max_tokens:
+ self._remove_lowest_priority_memory()
+
+ def _expire_old_memories(self):
+ """按TTL清理过期记忆,并同步更新堆与token计数"""
+ if not self.memories:
+ return
+ cutoff_time = datetime.now() - timedelta(minutes=self.max_age_minutes)
+ # 过滤保留的记忆
+ kept: List[MemoryItem] = []
+ removed_token_sum = 0
+ for m in self.memories:
+ if m.timestamp >= cutoff_time:
+ kept.append(m)
+ else:
+ removed_token_sum += len(m.content.split())
+ if len(kept) == len(self.memories):
+ return
+ # 覆盖列表与token
+ self.memories = kept
+ self.current_tokens = max(0, self.current_tokens - removed_token_sum)
+ # 重建堆
+ self.memory_heap = []
+ for mem in self.memories:
+ priority = self._calculate_priority(mem)
+ heapq.heappush(self.memory_heap, (-priority, mem.timestamp, mem))
+
+ def _remove_lowest_priority_memory(self):
+ """删除优先级最低的记忆"""
+ if not self.memories:
+ return
+
+ # 找到优先级最低的记忆
+ lowest_priority = float('inf')
+ lowest_memory = None
+
+ for memory in self.memories:
+ priority = self._calculate_priority(memory)
+ if priority < lowest_priority:
+ lowest_priority = priority
+ lowest_memory = memory
+
+ if lowest_memory:
+ self.remove(lowest_memory.id)
+
+ def _update_heap_priority(self, memory: MemoryItem):
+ """更新堆中记忆的优先级"""
+ # 简单实现:重建堆
+ self.memory_heap = []
+ for mem in self.memories:
+ priority = self._calculate_priority(mem)
+ heapq.heappush(self.memory_heap, (-priority, mem.timestamp, mem))
+
+ def _mark_deleted_in_heap(self, memory_id: str):
+ """在堆中标记删除的记忆"""
+ # 由于heapq不支持直接删除,我们标记为已删除
+ # 在后续操作中会被清理
+ pass
diff --git a/Co-creation-projects/aug618-Praxis/pyproject.toml b/Co-creation-projects/aug618-Praxis/pyproject.toml
new file mode 100644
index 00000000..b844a3d0
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/pyproject.toml
@@ -0,0 +1,14 @@
+[project]
+name = "praxis"
+version = "0.1.0"
+description = "A local-first AI coding agent powered by ReAct reasoning"
+readme = "README.md"
+requires-python = ">=3.12"
+dependencies = [
+ "hello-agents[all]==0.2.7",
+ "openai>=1.0.0",
+ "pydantic>=2.0.0",
+ "python-dotenv>=1.0.0",
+ "textual>=7.4.0",
+ "tiktoken>=0.5.0",
+]
diff --git a/Co-creation-projects/aug618-Praxis/requirements.txt b/Co-creation-projects/aug618-Praxis/requirements.txt
new file mode 100644
index 00000000..693c3efe
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/requirements.txt
@@ -0,0 +1,6 @@
+hello-agents[all]==0.2.7
+openai>=1.0.0
+pydantic>=2.0.0
+python-dotenv>=1.0.0
+textual>=7.4.0
+tiktoken>=0.5.0
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/tools/__init__.py b/Co-creation-projects/aug618-Praxis/tools/__init__.py
new file mode 100644
index 00000000..1dfd7401
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/__init__.py
@@ -0,0 +1,10 @@
+"""Tools package.
+
+Keep this module lightweight to avoid importing optional dependencies at import time.
+Import concrete tools directly, e.g. `from tools.builtin.terminal_tool import TerminalTool`.
+"""
+
+from .base import Tool, ToolParameter
+from .registry import ToolRegistry, global_registry
+
+__all__ = ["Tool", "ToolParameter", "ToolRegistry", "global_registry"]
diff --git a/Co-creation-projects/aug618-Praxis/tools/async_executor.py b/Co-creation-projects/aug618-Praxis/tools/async_executor.py
new file mode 100644
index 00000000..246c1350
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/async_executor.py
@@ -0,0 +1,184 @@
+"""异步工具执行器 - HelloAgents异步工具执行支持"""
+
+import asyncio
+import concurrent.futures
+from typing import Dict, Any, List
+from .registry import ToolRegistry
+
+
+class AsyncToolExecutor:
+ """异步工具执行器"""
+
+ def __init__(self, registry: ToolRegistry, max_workers: int = 4):
+ self.registry = registry
+ self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=max_workers)
+
+ async def execute_tool_async(self, tool_name: str, input_data: str) -> str:
+ """异步执行单个工具"""
+ loop = asyncio.get_event_loop()
+
+ def _execute():
+ return self.registry.execute_tool(tool_name, input_data)
+
+ try:
+ result = await loop.run_in_executor(self.executor, _execute)
+ return result
+ except Exception as e:
+ return f"❌ 工具 '{tool_name}' 异步执行失败: {e}"
+
+ async def execute_tools_parallel(self, tasks: List[Dict[str, str]]) -> List[Dict[str, Any]]:
+ """
+ 并行执行多个工具
+
+ Args:
+ tasks: 任务列表,每个任务包含 tool_name 和 input_data
+
+ Returns:
+ 执行结果列表,包含任务信息和结果
+ """
+ print(f"🚀 开始并行执行 {len(tasks)} 个工具任务")
+
+ # 创建异步任务
+ async_tasks = []
+ for i, task in enumerate(tasks):
+ tool_name = task.get("tool_name")
+ input_data = task.get("input_data", "")
+
+ if not tool_name:
+ continue
+
+ print(f"📝 创建任务 {i+1}: {tool_name}")
+ async_task = self.execute_tool_async(tool_name, input_data)
+ async_tasks.append((i, task, async_task))
+
+ # 等待所有任务完成
+ results = []
+ for i, task, async_task in async_tasks:
+ try:
+ result = await async_task
+ results.append({
+ "task_id": i,
+ "tool_name": task["tool_name"],
+ "input_data": task["input_data"],
+ "result": result,
+ "status": "success"
+ })
+ print(f"✅ 任务 {i+1} 完成: {task['tool_name']}")
+ except Exception as e:
+ results.append({
+ "task_id": i,
+ "tool_name": task["tool_name"],
+ "input_data": task["input_data"],
+ "result": str(e),
+ "status": "error"
+ })
+ print(f"❌ 任务 {i+1} 失败: {task['tool_name']} - {e}")
+
+ print(f"🎉 并行执行完成,成功: {sum(1 for r in results if r['status'] == 'success')}/{len(results)}")
+ return results
+
+ async def execute_tools_batch(self, tool_name: str, input_list: List[str]) -> List[Dict[str, Any]]:
+ """
+ 批量执行同一个工具
+
+ Args:
+ tool_name: 工具名称
+ input_list: 输入数据列表
+
+ Returns:
+ 执行结果列表
+ """
+ tasks = [
+ {"tool_name": tool_name, "input_data": input_data}
+ for input_data in input_list
+ ]
+ return await self.execute_tools_parallel(tasks)
+
+ def close(self):
+ """关闭执行器"""
+ self.executor.shutdown(wait=True)
+ print("🔒 异步工具执行器已关闭")
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ self.close()
+
+
+# 便捷函数
+async def run_parallel_tools(registry: ToolRegistry, tasks: List[Dict[str, str]], max_workers: int = 4) -> List[Dict[str, Any]]:
+ """
+ 便捷函数:并行执行多个工具
+
+ Args:
+ registry: 工具注册表
+ tasks: 任务列表
+ max_workers: 最大工作线程数
+
+ Returns:
+ 执行结果列表
+ """
+ async with AsyncToolExecutor(registry, max_workers) as executor:
+ return await executor.execute_tools_parallel(tasks)
+
+
+async def run_batch_tool(registry: ToolRegistry, tool_name: str, input_list: List[str], max_workers: int = 4) -> List[Dict[str, Any]]:
+ """
+ 便捷函数:批量执行同一个工具
+
+ Args:
+ registry: 工具注册表
+ tool_name: 工具名称
+ input_list: 输入数据列表
+ max_workers: 最大工作线程数
+
+ Returns:
+ 执行结果列表
+ """
+ async with AsyncToolExecutor(registry, max_workers) as executor:
+ return await executor.execute_tools_batch(tool_name, input_list)
+
+
+# 同步包装函数(为了兼容性)
+def run_parallel_tools_sync(registry: ToolRegistry, tasks: List[Dict[str, str]], max_workers: int = 4) -> List[Dict[str, Any]]:
+ """同步版本的并行工具执行"""
+ return asyncio.run(run_parallel_tools(registry, tasks, max_workers))
+
+
+def run_batch_tool_sync(registry: ToolRegistry, tool_name: str, input_list: List[str], max_workers: int = 4) -> List[Dict[str, Any]]:
+ """同步版本的批量工具执行"""
+ return asyncio.run(run_batch_tool(registry, tool_name, input_list, max_workers))
+
+
+# 示例函数
+async def demo_parallel_execution():
+ """演示并行执行的示例"""
+ from .registry import ToolRegistry
+
+ # 创建注册表(这里假设已经注册了工具)
+ registry = ToolRegistry()
+
+ # 定义并行任务
+ tasks = [
+ {"tool_name": "my_calculator", "input_data": "2 + 2"},
+ {"tool_name": "my_calculator", "input_data": "3 * 4"},
+ {"tool_name": "my_calculator", "input_data": "sqrt(16)"},
+ {"tool_name": "my_calculator", "input_data": "10 / 2"},
+ ]
+
+ # 并行执行
+ results = await run_parallel_tools(registry, tasks)
+
+ # 显示结果
+ print("\n📊 并行执行结果:")
+ for result in results:
+ status_icon = "✅" if result["status"] == "success" else "❌"
+ print(f"{status_icon} {result['tool_name']}({result['input_data']}) = {result['result']}")
+
+ return results
+
+
+if __name__ == "__main__":
+ # 运行演示
+ asyncio.run(demo_parallel_execution())
diff --git a/Co-creation-projects/aug618-Praxis/tools/base.py b/Co-creation-projects/aug618-Praxis/tools/base.py
new file mode 100644
index 00000000..42382252
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/base.py
@@ -0,0 +1,49 @@
+"""工具基类"""
+
+from abc import ABC, abstractmethod
+from typing import Dict, Any, List
+from pydantic import BaseModel
+
+class ToolParameter(BaseModel):
+ """工具参数定义"""
+ name: str
+ type: str
+ description: str
+ required: bool = True
+ default: Any = None
+
+class Tool(ABC):
+ """工具基类"""
+
+ def __init__(self, name: str, description: str):
+ self.name = name
+ self.description = description
+
+ @abstractmethod
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """执行工具"""
+ pass
+
+ @abstractmethod
+ def get_parameters(self) -> List[ToolParameter]:
+ """获取工具参数定义"""
+ pass
+
+ def validate_parameters(self, parameters: Dict[str, Any]) -> bool:
+ """验证参数"""
+ required_params = [p.name for p in self.get_parameters() if p.required]
+ return all(param in parameters for param in required_params)
+
+ def to_dict(self) -> Dict[str, Any]:
+ """转换为字典格式"""
+ return {
+ "name": self.name,
+ "description": self.description,
+ "parameters": [param.model_dump() for param in self.get_parameters()]
+ }
+
+ def __str__(self) -> str:
+ return f"Tool(name={self.name})"
+
+ def __repr__(self) -> str:
+ return self.__str__()
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/__init__.py b/Co-creation-projects/aug618-Praxis/tools/builtin/__init__.py
new file mode 100644
index 00000000..c26d90d0
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/__init__.py
@@ -0,0 +1,23 @@
+"""Built-in tools.
+
+Keep this module lightweight: avoid importing optional dependencies at import time.
+Import concrete tools directly, e.g. `from tools.builtin.note_tool import NoteTool`.
+"""
+
+__all__ = [
+ "SearchTool",
+ "CalculatorTool",
+ "MemoryTool",
+ "RAGTool",
+ "NoteTool",
+ "TerminalTool",
+ "MCPTool",
+ "A2ATool",
+ "ANPTool",
+ "BFCLEvaluationTool",
+ "GAIAEvaluationTool",
+ "LLMJudgeTool",
+ "WinRateTool",
+ "ContextFetchTool",
+ "SkillsTool",
+]
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/context_fetch_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/context_fetch_tool.py
new file mode 100644
index 00000000..d57555db
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/context_fetch_tool.py
@@ -0,0 +1,300 @@
+"""上下文获取工具 - 让模型按需获取扩展上下文
+
+设计理念(借鉴 Claude Code):
+- 保底上下文由 ContextBuilder 自动注入(系统提示、对话历史、上次工具摘要)
+- 扩展上下文通过此工具按需获取(notes、memory、files、tests)
+- 模型自行决定何时需要更多证据,避免盲目全局扫描
+"""
+
+from typing import Dict, Any, List, Optional
+import subprocess
+import os
+
+from ..base import Tool, ToolParameter
+
+
+class ContextFetchTool(Tool):
+ """上下文获取工具
+
+ 让模型按需获取扩展上下文,支持多种数据源:
+ - notes: 检索笔记(blocker、insight、decision 等)
+ - memory: 检索情景记忆(之前的对话/经验)
+ - files: 搜索代码文件(rg + 上下文行)
+ - tests: 获取最近测试失败信息
+
+ 使用场景:
+ - 模型发现证据不足时主动调用
+ - 提到类名/函数名/错误栈时获取相关代码
+ - 询问"之前做了什么"时检索记忆
+ """
+
+ def __init__(
+ self,
+ workspace: str,
+ note_tool: Optional[Any] = None,
+ memory_tool: Optional[Any] = None,
+ max_tokens_per_source: int = 800,
+ context_lines: int = 5, # 命中行前后各取 k 行
+ ):
+ super().__init__(
+ name="context_fetch",
+ description=(
+ "获取扩展上下文。当保底上下文不足以回答问题时调用。"
+ "可指定数据源:notes(笔记)、memory(记忆)、files(代码文件)、tests(测试结果)。"
+ "返回结构化的证据块。"
+ ),
+ )
+ self.workspace = workspace
+ self.note_tool = note_tool
+ self.memory_tool = memory_tool
+ self.max_tokens_per_source = max_tokens_per_source
+ self.context_lines = context_lines
+
+ # 缓存最近的查询结果,避免重复查询
+ self._cache: Dict[str, str] = {}
+ self._cache_max_size = 20
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """遵循基类接口返回参数定义"""
+ return [
+ ToolParameter(
+ name="sources",
+ type="array",
+ description="要查询的数据源列表,可选: notes, memory, files, tests",
+ required=True,
+ ),
+ ToolParameter(
+ name="query",
+ type="string",
+ description="搜索关键词/符号名/错误栈片段",
+ required=True,
+ ),
+ ToolParameter(
+ name="paths",
+ type="string",
+ description="限定文件搜索范围的 glob 模式,如 'src/**/*.py'",
+ required=False,
+ ),
+ ToolParameter(
+ name="budget_tokens",
+ type="integer",
+ description="单个数据源的 token 上限,默认 800",
+ required=False,
+ ),
+ ]
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """执行上下文获取"""
+ sources = parameters.get("sources", [])
+ query = parameters.get("query", "")
+ paths = parameters.get("paths", "")
+ budget = parameters.get("budget_tokens", self.max_tokens_per_source)
+
+ if not sources or not query:
+ return "错误:必须指定 sources 和 query 参数"
+
+ # 检查缓存
+ cache_key = f"{','.join(sorted(sources))}|{query}|{paths}"
+ if cache_key in self._cache:
+ return f"[缓存命中]\n{self._cache[cache_key]}"
+
+ results: List[str] = []
+
+ for source in sources:
+ if source == "notes":
+ result = self._fetch_notes(query, budget)
+ elif source == "memory":
+ result = self._fetch_memory(query, budget)
+ elif source == "files":
+ result = self._fetch_files(query, paths, budget)
+ elif source == "tests":
+ result = self._fetch_tests(query, budget)
+ else:
+ result = f"[{source}] 未知数据源"
+
+ if result:
+ results.append(result)
+
+ output = "\n\n".join(results) if results else "未找到相关上下文"
+
+ # 更新缓存
+ if len(self._cache) >= self._cache_max_size:
+ # 简单 LRU:删除最早的
+ oldest_key = next(iter(self._cache))
+ del self._cache[oldest_key]
+ self._cache[cache_key] = output
+
+ return output
+
+ def _fetch_notes(self, query: str, budget: int) -> str:
+ """从笔记中检索"""
+ if not self.note_tool:
+ return "[notes] 笔记工具未配置"
+
+ try:
+ # 搜索相关笔记
+ result = self.note_tool.run({
+ "action": "search",
+ "query": query,
+ "limit": 5,
+ })
+ if result and "未找到" not in result:
+ return f"[notes] 相关笔记:\n{self._truncate(result, budget)}"
+ return "[notes] 未找到相关笔记"
+ except Exception as e:
+ return f"[notes] 检索失败: {e}"
+
+ def _fetch_memory(self, query: str, budget: int) -> str:
+ """从记忆中检索"""
+ if not self.memory_tool:
+ return "[memory] 记忆工具未配置"
+
+ try:
+ result = self.memory_tool.run({
+ "action": "search",
+ "query": query,
+ "memory_types": getattr(self.memory_tool, "memory_types", ["episodic"]),
+ "limit": 5,
+ "min_importance": 0.0,
+ })
+ if result and "未找到" not in result:
+ return f"[memory] 相关记忆:\n{self._truncate(result, budget)}"
+ return "[memory] 未找到相关记忆"
+ except Exception as e:
+ return f"[memory] 检索失败: {e}"
+
+ def _fetch_files(self, query: str, paths: str, budget: int) -> str:
+ """从代码文件中检索"""
+ try:
+ # 使用 ripgrep 搜索
+ cmd = ["rg", "--color=never", "-n", "-C", str(self.context_lines)]
+
+ if paths:
+ cmd.extend(["-g", paths])
+
+ cmd.append(query)
+ cmd.append(self.workspace)
+
+ result = subprocess.run(
+ cmd,
+ capture_output=True,
+ text=True,
+ timeout=10,
+ cwd=self.workspace,
+ )
+
+ output = result.stdout.strip()
+ if output:
+ # 结构化输出
+ lines = output.split("\n")
+ # 按文件分组
+ grouped = self._group_by_file(lines)
+ formatted = self._format_file_results(grouped, budget)
+ return f"[files] 代码搜索结果:\n{formatted}"
+ return f"[files] 未找到匹配 '{query}' 的内容"
+ except subprocess.TimeoutExpired:
+ return "[files] 搜索超时"
+ except FileNotFoundError:
+ # ripgrep 未安装,降级到 grep
+ return self._fetch_files_fallback(query, paths, budget)
+ except Exception as e:
+ return f"[files] 搜索失败: {e}"
+
+ def _fetch_files_fallback(self, query: str, paths: str, budget: int) -> str:
+ """ripgrep 不可用时的降级方案"""
+ try:
+ cmd = f"grep -rn '{query}' {self.workspace}"
+ if paths:
+ cmd = f"find {self.workspace} -path '{paths}' -type f | xargs grep -n '{query}'"
+
+ result = subprocess.run(
+ cmd,
+ shell=True,
+ capture_output=True,
+ text=True,
+ timeout=10,
+ )
+ output = result.stdout.strip()
+ if output:
+ return f"[files] grep 结果:\n{self._truncate(output, budget)}"
+ return f"[files] 未找到匹配 '{query}' 的内容"
+ except Exception as e:
+ return f"[files] grep 搜索失败: {e}"
+
+ def _fetch_tests(self, query: str, budget: int) -> str:
+ """获取测试相关信息"""
+ # 查找最近的测试输出/日志
+ test_patterns = [
+ ".pytest_cache/v/cache/lastfailed",
+ "test-results.xml",
+ ".coverage",
+ ]
+
+ results = []
+ for pattern in test_patterns:
+ path = os.path.join(self.workspace, pattern)
+ if os.path.exists(path):
+ try:
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
+ content = f.read()
+ if query.lower() in content.lower():
+ results.append(f"[tests] {pattern}:\n{self._truncate(content, budget // 2)}")
+ except Exception:
+ pass
+
+ if results:
+ return "\n".join(results)
+ return "[tests] 未找到相关测试信息"
+
+ def _group_by_file(self, lines: List[str]) -> Dict[str, List[str]]:
+ """按文件分组 ripgrep 输出"""
+ grouped: Dict[str, List[str]] = {}
+ current_file = None
+
+ for line in lines:
+ if ":" in line:
+ # 格式: file:line:content 或 file-line-content
+ parts = line.split(":", 2) if ":" in line else line.split("-", 2)
+ if len(parts) >= 2:
+ file_path = parts[0]
+ if file_path != current_file:
+ current_file = file_path
+ grouped[current_file] = []
+ grouped[current_file].append(line)
+ elif current_file:
+ grouped[current_file].append(line)
+
+ return grouped
+
+ def _format_file_results(self, grouped: Dict[str, List[str]], budget: int) -> str:
+ """格式化文件搜索结果"""
+ output_parts = []
+ tokens_used = 0
+ tokens_per_file = budget // max(len(grouped), 1)
+
+ for file_path, lines in grouped.items():
+ content = "\n".join(lines)
+ truncated = self._truncate(content, tokens_per_file)
+
+ # 相对路径
+ rel_path = file_path.replace(self.workspace, "").lstrip("/")
+ output_parts.append(f"--- {rel_path} ---\n{truncated}")
+
+ tokens_used += len(truncated) // 4 # 粗略估算
+ if tokens_used >= budget:
+ output_parts.append("...(更多结果已截断)...")
+ break
+
+ return "\n\n".join(output_parts)
+
+ def _truncate(self, text: str, max_tokens: int) -> str:
+ """截断文本到指定 token 上限"""
+ # 粗略估算:1 token ≈ 4 字符(英文),2 字符(中文)
+ max_chars = max_tokens * 3
+ if len(text) <= max_chars:
+ return text
+ return text[:max_chars] + "\n...(已截断)..."
+
+ def clear_cache(self):
+ """清空缓存"""
+ self._cache.clear()
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/mcp_wrapper_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/mcp_wrapper_tool.py
new file mode 100644
index 00000000..d06cabc7
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/mcp_wrapper_tool.py
@@ -0,0 +1,119 @@
+"""
+MCP工具包装器 - 将单个MCP工具包装成HelloAgents Tool
+
+这个模块将MCP服务器的每个工具展开为独立的HelloAgents Tool对象,
+使得Agent可以像调用普通工具一样调用MCP工具。
+"""
+
+from typing import Dict, Any, Optional, List
+from ..base import Tool, ToolParameter
+
+
+class MCPWrappedTool(Tool):
+ """
+ MCP工具包装器 - 将单个MCP工具包装成HelloAgents Tool
+
+ 这个类将MCP服务器的一个工具(如 read_file)包装成一个独立的Tool对象。
+ Agent调用时只需提供参数,无需了解MCP的内部结构。
+
+ 示例:
+ >>> # 内部使用,由MCPTool自动创建
+ >>> wrapped_tool = MCPWrappedTool(
+ ... mcp_tool=mcp_tool_instance,
+ ... tool_info={
+ ... "name": "read_file",
+ ... "description": "Read a file...",
+ ... "input_schema": {...}
+ ... }
+ ... )
+ """
+
+ def __init__(self,
+ mcp_tool: 'MCPTool', # type: ignore
+ tool_info: Dict[str, Any],
+ prefix: str = ""):
+ """
+ 初始化MCP包装工具
+
+ Args:
+ mcp_tool: 父MCP工具实例
+ tool_info: MCP工具信息(包含name, description, input_schema)
+ prefix: 工具名前缀(如 "filesystem_")
+ """
+ self.mcp_tool = mcp_tool
+ self.tool_info = tool_info
+ self.mcp_tool_name = tool_info.get('name', 'unknown')
+
+ # 构建工具名:prefix + mcp_tool_name
+ tool_name = f"{prefix}{self.mcp_tool_name}" if prefix else self.mcp_tool_name
+
+ # 获取描述
+ description = tool_info.get('description', f'MCP工具: {self.mcp_tool_name}')
+
+ # 解析参数schema
+ self._parameters = self._parse_input_schema(tool_info.get('input_schema', {}))
+
+ # 初始化父类
+ super().__init__(
+ name=tool_name,
+ description=description
+ )
+
+ def _parse_input_schema(self, input_schema: Dict[str, Any]) -> List[ToolParameter]:
+ """
+ 将MCP的input_schema转换为HelloAgents的ToolParameter列表
+
+ Args:
+ input_schema: MCP工具的input_schema(JSON Schema格式)
+
+ Returns:
+ ToolParameter列表
+ """
+ parameters = []
+
+ properties = input_schema.get('properties', {})
+ required_fields = input_schema.get('required', [])
+
+ for param_name, param_info in properties.items():
+ param_type = param_info.get('type', 'string')
+ param_desc = param_info.get('description', '')
+ is_required = param_name in required_fields
+
+ parameters.append(ToolParameter(
+ name=param_name,
+ type=param_type, # 直接使用JSON Schema的类型字符串
+ description=param_desc,
+ required=is_required
+ ))
+
+ return parameters
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """
+ 获取工具参数定义
+
+ Returns:
+ ToolParameter列表
+ """
+ return self._parameters
+
+ def run(self, params: Dict[str, Any]) -> str:
+ """
+ 执行MCP工具
+
+ Args:
+ params: 工具参数(直接传递给MCP工具)
+
+ Returns:
+ 执行结果
+ """
+ # 构建MCP调用参数
+ mcp_params = {
+ "action": "call_tool",
+ "tool_name": self.mcp_tool_name,
+ "arguments": params
+ }
+
+ # 调用父MCP工具
+ return self.mcp_tool.run(mcp_params)
+
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/memory_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/memory_tool.py
new file mode 100644
index 00000000..2eadd3d3
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/memory_tool.py
@@ -0,0 +1,453 @@
+"""记忆工具
+
+为HelloAgents框架提供记忆能力的工具实现。
+可以作为工具添加到任何Agent中,让Agent具备记忆功能。
+"""
+
+from typing import Dict, Any, List
+from datetime import datetime
+
+from ..base import Tool, ToolParameter
+from memory import MemoryManager, MemoryConfig
+
+class MemoryTool(Tool):
+ """记忆工具
+
+ 为Agent提供记忆功能:
+ - 添加记忆
+ - 检索相关记忆
+ - 获取记忆摘要
+ - 管理记忆生命周期
+ """
+
+ def __init__(
+ self,
+ user_id: str = "default_user",
+ memory_config: MemoryConfig = None,
+ memory_types: List[str] = None
+ ):
+ super().__init__(
+ name="memory",
+ description="记忆工具 - 可以存储和检索对话历史、知识和经验"
+ )
+
+ # 初始化记忆管理器
+ self.memory_config = memory_config or MemoryConfig()
+ self.memory_types = memory_types or ["working", "episodic", "semantic"]
+
+ self.memory_manager = MemoryManager(
+ config=self.memory_config,
+ user_id=user_id,
+ enable_working="working" in self.memory_types,
+ enable_episodic="episodic" in self.memory_types,
+ enable_semantic="semantic" in self.memory_types,
+ enable_perceptual="perceptual" in self.memory_types
+ )
+
+ # 会话状态
+ self.current_session_id = None
+ self.conversation_count = 0
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """执行工具 - Tool基类要求的接口
+
+ Args:
+ parameters: 工具参数字典,必须包含action参数
+
+ Returns:
+ 执行结果字符串
+ """
+ if not self.validate_parameters(parameters):
+ return "❌ 参数验证失败:缺少必需的参数"
+
+ action = parameters.get("action")
+ # 移除action参数,传递其余参数给execute方法
+ kwargs = {k: v for k, v in parameters.items() if k != "action"}
+
+ return self.execute(action, **kwargs)
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """获取工具参数定义 - Tool基类要求的接口"""
+ return [
+ ToolParameter(
+ name="action",
+ type="string",
+ description=(
+ "要执行的操作:"
+ "add(添加记忆), search(搜索记忆), summary(获取摘要), stats(获取统计), "
+ "update(更新记忆), remove(删除记忆), forget(遗忘记忆), consolidate(整合记忆), clear_all(清空所有记忆)"
+ ),
+ required=True
+ ),
+ ToolParameter(name="content", type="string", description="记忆内容(add/update时可用;感知记忆可作描述)", required=False),
+ ToolParameter(name="query", type="string", description="搜索查询(search时可用)", required=False),
+ ToolParameter(name="memory_type", type="string", description="记忆类型:working, episodic, semantic, perceptual(默认:working)", required=False, default="working"),
+ ToolParameter(name="importance", type="number", description="重要性分数,0.0-1.0(add/update时可用)", required=False),
+ ToolParameter(name="limit", type="integer", description="搜索结果数量限制(默认:5)", required=False, default=5),
+ ToolParameter(name="memory_id", type="string", description="目标记忆ID(update/remove时必需)", required=False),
+ ToolParameter(name="file_path", type="string", description="感知记忆:本地文件路径(image/audio)", required=False),
+ ToolParameter(name="modality", type="string", description="感知记忆模态:text/image/audio(不传则按扩展名推断)", required=False),
+ ToolParameter(name="strategy", type="string", description="遗忘策略:importance_based/time_based/capacity_based(forget时可用)", required=False, default="importance_based"),
+ ToolParameter(name="threshold", type="number", description="遗忘阈值(forget时可用,默认0.1)", required=False, default=0.1),
+ ToolParameter(name="max_age_days", type="integer", description="最大保留天数(forget策略为time_based时可用)", required=False, default=30),
+ ToolParameter(name="from_type", type="string", description="整合来源类型(consolidate时可用,默认working)", required=False, default="working"),
+ ToolParameter(name="to_type", type="string", description="整合目标类型(consolidate时可用,默认episodic)", required=False, default="episodic"),
+ ToolParameter(name="importance_threshold", type="number", description="整合重要性阈值(默认0.7)", required=False, default=0.7),
+ ]
+
+ def execute(self, action: str, **kwargs) -> str:
+ """执行记忆操作
+
+ 支持的操作:
+ - add: 添加记忆
+ - search: 搜索记忆
+ - summary: 获取记忆摘要
+ - stats: 获取统计信息
+ """
+
+ if action == "add":
+ return self._add_memory(**kwargs)
+ elif action == "search":
+ return self._search_memory(**kwargs)
+ elif action == "summary":
+ return self._get_summary(**kwargs)
+ elif action == "stats":
+ return self._get_stats()
+ elif action == "update":
+ return self._update_memory(**kwargs)
+ elif action == "remove":
+ return self._remove_memory(**kwargs)
+ elif action == "forget":
+ return self._forget(**kwargs)
+ elif action == "consolidate":
+ return self._consolidate(**kwargs)
+ elif action == "clear_all":
+ return self._clear_all()
+ else:
+ return f"不支持的操作: {action}。支持的操作: add, search, summary, stats, update, remove, forget, consolidate, clear_all"
+
+ def _add_memory(
+ self,
+ content: str = "",
+ memory_type: str = "working",
+ importance: float = 0.5,
+ file_path: str = None,
+ modality: str = None,
+ **metadata
+ ) -> str:
+ """添加记忆"""
+ try:
+ # 确保会话ID存在
+ if self.current_session_id is None:
+ self.current_session_id = f"session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
+
+ # 感知记忆文件支持:注入 raw_data 与模态
+ if memory_type == "perceptual" and file_path:
+ inferred = modality or self._infer_modality(file_path)
+ metadata.setdefault("modality", inferred)
+ metadata.setdefault("raw_data", file_path)
+
+ # 添加会话信息到元数据
+ metadata.update({
+ "session_id": self.current_session_id,
+ "timestamp": datetime.now().isoformat()
+ })
+
+ memory_id = self.memory_manager.add_memory(
+ content=content,
+ memory_type=memory_type,
+ importance=importance,
+ metadata=metadata,
+ auto_classify=False # 禁用自动分类,使用明确指定的类型
+ )
+
+ return f"✅ 记忆已添加 (ID: {memory_id[:8]}...)"
+
+ except Exception as e:
+ return f"❌ 添加记忆失败: {str(e)}"
+
+ def _infer_modality(self, path: str) -> str:
+ """根据扩展名推断模态(默认image/audio/text)"""
+ try:
+ ext = (path.rsplit('.', 1)[-1] or '').lower()
+ if ext in {"png", "jpg", "jpeg", "bmp", "gif", "webp"}:
+ return "image"
+ if ext in {"mp3", "wav", "flac", "m4a", "ogg"}:
+ return "audio"
+ return "text"
+ except Exception:
+ return "text"
+
+ def _search_memory(
+ self,
+ query: str,
+ limit: int = 5,
+ memory_types: List[str] = None,
+ memory_type: str = None, # 添加单数形式的参数支持
+ min_importance: float = 0.1
+ ) -> str:
+ """搜索记忆"""
+ try:
+ # 处理单数形式的memory_type参数
+ if memory_type and not memory_types:
+ memory_types = [memory_type]
+
+ results = self.memory_manager.retrieve_memories(
+ query=query,
+ limit=limit,
+ memory_types=memory_types,
+ min_importance=min_importance
+ )
+
+ if not results:
+ return f"🔍 未找到与 '{query}' 相关的记忆"
+
+ # 格式化结果
+ formatted_results = []
+ formatted_results.append(f"🔍 找到 {len(results)} 条相关记忆:")
+
+ for i, memory in enumerate(results, 1):
+ memory_type_label = {
+ "working": "工作记忆",
+ "episodic": "情景记忆",
+ "semantic": "语义记忆",
+ "perceptual": "感知记忆"
+ }.get(memory.memory_type, memory.memory_type)
+
+ content_preview = memory.content[:80] + "..." if len(memory.content) > 80 else memory.content
+ formatted_results.append(
+ f"{i}. [{memory_type_label}] {content_preview} (重要性: {memory.importance:.2f})"
+ )
+
+ return "\n".join(formatted_results)
+
+ except Exception as e:
+ return f"❌ 搜索记忆失败: {str(e)}"
+
+ def _get_summary(self, limit: int = 10) -> str:
+ """获取记忆摘要"""
+ try:
+ stats = self.memory_manager.get_memory_stats()
+
+ summary_parts = [
+ f"📊 记忆系统摘要",
+ f"总记忆数: {stats['total_memories']}",
+ f"当前会话: {self.current_session_id or '未开始'}",
+ f"对话轮次: {self.conversation_count}"
+ ]
+
+ # 各类型记忆统计
+ if stats['memories_by_type']:
+ summary_parts.append("\n📋 记忆类型分布:")
+ for memory_type, type_stats in stats['memories_by_type'].items():
+ count = type_stats.get('count', 0)
+ avg_importance = type_stats.get('avg_importance', 0)
+ type_label = {
+ "working": "工作记忆",
+ "episodic": "情景记忆",
+ "semantic": "语义记忆",
+ "perceptual": "感知记忆"
+ }.get(memory_type, memory_type)
+
+ summary_parts.append(f" • {type_label}: {count} 条 (平均重要性: {avg_importance:.2f})")
+
+ # 获取重要记忆 - 修复重复问题
+ important_memories = self.memory_manager.retrieve_memories(
+ query="",
+ memory_types=None, # 从所有类型中检索
+ limit=limit * 3, # 获取更多候选,然后去重
+ min_importance=0.5 # 降低阈值以获取更多记忆
+ )
+
+ if important_memories:
+ # 去重:使用记忆ID和内容双重去重
+ seen_ids = set()
+ seen_contents = set()
+ unique_memories = []
+
+ for memory in important_memories:
+ # 使用ID去重
+ if memory.id in seen_ids:
+ continue
+
+ # 使用内容去重(防止相同内容的不同记忆)
+ content_key = memory.content.strip().lower()
+ if content_key in seen_contents:
+ continue
+
+ seen_ids.add(memory.id)
+ seen_contents.add(content_key)
+ unique_memories.append(memory)
+
+ # 按重要性排序
+ unique_memories.sort(key=lambda x: x.importance, reverse=True)
+ summary_parts.append(f"\n⭐ 重要记忆 (前{min(limit, len(unique_memories))}条):")
+
+ for i, memory in enumerate(unique_memories[:limit], 1):
+ content_preview = memory.content[:60] + "..." if len(memory.content) > 60 else memory.content
+ summary_parts.append(f" {i}. {content_preview} (重要性: {memory.importance:.2f})")
+
+ return "\n".join(summary_parts)
+
+ except Exception as e:
+ return f"❌ 获取摘要失败: {str(e)}"
+
+ def _get_stats(self) -> str:
+ """获取统计信息"""
+ try:
+ stats = self.memory_manager.get_memory_stats()
+
+ stats_info = [
+ f"📈 记忆系统统计",
+ f"总记忆数: {stats['total_memories']}",
+ f"启用的记忆类型: {', '.join(stats['enabled_types'])}",
+ f"会话ID: {self.current_session_id or '未开始'}",
+ f"对话轮次: {self.conversation_count}"
+ ]
+
+ return "\n".join(stats_info)
+
+ except Exception as e:
+ return f"❌ 获取统计信息失败: {str(e)}"
+
+ def auto_record_conversation(self, user_input: str, agent_response: str):
+ """自动记录对话
+
+ 这个方法可以被Agent调用来自动记录对话历史
+ """
+ self.conversation_count += 1
+ # 记录用户输入
+ self._add_memory(
+ content=f"用户: {user_input}",
+ memory_type="working",
+ importance=0.6,
+ type="user_input",
+ conversation_id=self.conversation_count
+ )
+
+ # 记录Agent响应
+ self._add_memory(
+ content=f"助手: {agent_response}",
+ memory_type="working",
+ importance=0.7,
+ type="agent_response",
+ conversation_id=self.conversation_count
+ )
+
+ # 如果是重要对话,记录为情景记忆
+ if len(agent_response) > 100 or "重要" in user_input or "记住" in user_input:
+ interaction_content = f"对话 - 用户: {user_input}\n助手: {agent_response}"
+ self._add_memory(
+ content=interaction_content,
+ memory_type="episodic",
+ importance=0.8,
+ type="interaction",
+ conversation_id=self.conversation_count
+ )
+
+ def _update_memory(self, memory_id: str, content: str = None, importance: float = None, **metadata) -> str:
+ """更新记忆"""
+ try:
+ success = self.memory_manager.update_memory(
+ memory_id=memory_id,
+ content=content,
+ importance=importance,
+ metadata=metadata or None
+ )
+ return "✅ 记忆已更新" if success else "⚠️ 未找到要更新的记忆"
+ except Exception as e:
+ return f"❌ 更新记忆失败: {str(e)}"
+
+ def _remove_memory(self, memory_id: str) -> str:
+ """删除记忆"""
+ try:
+ success = self.memory_manager.remove_memory(memory_id)
+ return "✅ 记忆已删除" if success else "⚠️ 未找到要删除的记忆"
+ except Exception as e:
+ return f"❌ 删除记忆失败: {str(e)}"
+
+ def _forget(self, strategy: str = "importance_based", threshold: float = 0.1, max_age_days: int = 30) -> str:
+ """遗忘记忆(支持多种策略)"""
+ try:
+ count = self.memory_manager.forget_memories(
+ strategy=strategy,
+ threshold=threshold,
+ max_age_days=max_age_days
+ )
+ return f"🧹 已遗忘 {count} 条记忆(策略: {strategy})"
+ except Exception as e:
+ return f"❌ 遗忘记忆失败: {str(e)}"
+
+ def _consolidate(self, from_type: str = "working", to_type: str = "episodic", importance_threshold: float = 0.7) -> str:
+ """整合记忆(将重要的短期记忆提升为长期记忆)"""
+ try:
+ count = self.memory_manager.consolidate_memories(
+ from_type=from_type,
+ to_type=to_type,
+ importance_threshold=importance_threshold,
+ )
+ return f"🔄 已整合 {count} 条记忆为长期记忆({from_type} → {to_type},阈值={importance_threshold})"
+ except Exception as e:
+ return f"❌ 整合记忆失败: {str(e)}"
+
+ def _clear_all(self) -> str:
+ """清空所有记忆"""
+ try:
+ self.memory_manager.clear_all_memories()
+ return "🧽 已清空所有记忆"
+ except Exception as e:
+ return f"❌ 清空记忆失败: {str(e)}"
+
+ def add_knowledge(self, content: str, importance: float = 0.9):
+ """添加知识到语义记忆
+
+ 便捷方法,用于添加重要知识
+ """
+ return self._add_memory(
+ content=content,
+ memory_type="semantic",
+ importance=importance,
+ knowledge_type="factual",
+ source="manual"
+ )
+
+ def get_context_for_query(self, query: str, limit: int = 3) -> str:
+ """为查询获取相关上下文
+
+ 这个方法可以被Agent调用来获取相关的记忆上下文
+ """
+ results = self.memory_manager.retrieve_memories(
+ query=query,
+ limit=limit,
+ min_importance=0.3
+ )
+
+ if not results:
+ return ""
+
+ context_parts = ["相关记忆:"]
+ for memory in results:
+ context_parts.append(f"- {memory.content}")
+
+ return "\n".join(context_parts)
+
+ def clear_session(self):
+ """清除当前会话"""
+ self.current_session_id = None
+ self.conversation_count = 0
+
+ # 清理工作记忆
+ wm = self.memory_manager.memory_types.get('working') if hasattr(self.memory_manager, 'memory_types') else None
+ if wm:
+ wm.clear()
+
+ def consolidate_memories(self):
+ """整合记忆"""
+ return self.memory_manager.consolidate_memories()
+
+ def forget_old_memories(self, max_age_days: int = 30):
+ """遗忘旧记忆"""
+ return self.memory_manager.forget_memories(
+ strategy="time_based",
+ max_age_days=max_age_days
+ )
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/note_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/note_tool.py
new file mode 100644
index 00000000..d5252452
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/note_tool.py
@@ -0,0 +1,518 @@
+"""NoteTool - 结构化笔记工具
+
+为Agent提供结构化笔记能力,支持:
+- 创建/读取/更新/删除笔记
+- 按类型组织(任务状态、结论、阻塞项、行动计划等)
+- 持久化存储(Markdown格式,带YAML前置元数据)
+- 搜索与过滤
+- 与MemoryTool集成(可选)
+
+使用场景:
+- 长时程任务的状态跟踪
+- 关键结论与依赖记录
+- 待办事项与行动计划
+- 项目知识沉淀
+
+笔记格式示例:
+```markdown
+---
+id: note_20250118_120000_0
+title: 项目进展
+type: task_state
+tags: [milestone, phase1]
+created_at: 2025-01-18T12:00:00
+updated_at: 2025-01-18T12:00:00
+---
+
+# 项目进展
+
+已完成需求分析,下一步:设计方案
+
+## 关键里程碑
+- [x] 需求收集
+- [ ] 方案设计
+```
+"""
+
+from typing import Dict, Any, List, Optional
+from datetime import datetime
+from pathlib import Path
+import json
+import os
+import re
+
+from ..base import Tool, ToolParameter
+
+
+class NoteTool(Tool):
+ """笔记工具
+
+ 为Agent提供结构化笔记管理能力,支持多种笔记类型:
+ - task_state: 任务状态
+ - conclusion: 关键结论
+ - blocker: 阻塞项
+ - action: 行动计划
+ - reference: 参考资料
+ - general: 通用笔记
+
+ 用法示例:
+ ```python
+ note_tool = NoteTool(workspace="./project_notes")
+
+ # 创建笔记
+ note_tool.run({
+ "action": "create",
+ "title": "项目进展",
+ "content": "已完成需求分析,下一步:设计方案",
+ "note_type": "task_state",
+ "tags": ["milestone", "phase1"]
+ })
+
+ # 读取笔记
+ notes = note_tool.run({"action": "list", "note_type": "task_state"})
+ ```
+ """
+
+ def __init__(
+ self,
+ workspace: str = "./notes",
+ auto_backup: bool = True,
+ max_notes: int = 1000
+ ):
+ super().__init__(
+ name="note",
+ description="笔记工具 - 创建、读取、更新、删除结构化笔记,支持任务状态、结论、阻塞项等类型"
+ )
+
+ self.workspace = Path(workspace)
+ self.auto_backup = auto_backup
+ self.max_notes = max_notes
+
+ # 确保工作目录存在
+ self.workspace.mkdir(parents=True, exist_ok=True)
+
+ # 笔记索引文件
+ self.index_file = self.workspace / "notes_index.json"
+ self._load_index()
+
+ def _load_index(self):
+ """加载笔记索引"""
+ if self.index_file.exists():
+ with open(self.index_file, 'r', encoding='utf-8') as f:
+ self.notes_index = json.load(f)
+ else:
+ self.notes_index = {
+ "notes": [],
+ "metadata": {
+ "created_at": datetime.now().isoformat(),
+ "total_notes": 0
+ }
+ }
+ self._save_index()
+
+ def _save_index(self):
+ """保存笔记索引"""
+ with open(self.index_file, 'w', encoding='utf-8') as f:
+ json.dump(self.notes_index, f, ensure_ascii=False, indent=2)
+
+ def _generate_note_id(self) -> str:
+ """生成笔记ID"""
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+ count = len(self.notes_index["notes"])
+ return f"note_{timestamp}_{count}"
+
+ def _get_note_path(self, note_id: str) -> Path:
+ """获取笔记文件路径"""
+ return self.workspace / f"{note_id}.md"
+
+ def _note_to_markdown(self, note: Dict[str, Any]) -> str:
+ """将笔记对象转换为Markdown格式"""
+ # YAML前置元数据
+ frontmatter = "---\n"
+ frontmatter += f"id: {note['id']}\n"
+ frontmatter += f"title: {note['title']}\n"
+ frontmatter += f"type: {note['type']}\n"
+ if note.get('tags'):
+ tags_str = json.dumps(note['tags'])
+ frontmatter += f"tags: {tags_str}\n"
+ frontmatter += f"created_at: {note['created_at']}\n"
+ frontmatter += f"updated_at: {note['updated_at']}\n"
+ frontmatter += "---\n\n"
+
+ # Markdown内容
+ content = f"# {note['title']}\n\n"
+ content += note['content']
+
+ return frontmatter + content
+
+ def _markdown_to_note(self, markdown_text: str) -> Dict[str, Any]:
+ """将Markdown文本解析为笔记对象"""
+ # 提取YAML前置元数据
+ frontmatter_match = re.match(r'^---\s*\n(.*?)\n---\s*\n', markdown_text, re.DOTALL)
+
+ if not frontmatter_match:
+ raise ValueError("无效的笔记格式:缺少YAML前置元数据")
+
+ frontmatter_text = frontmatter_match.group(1)
+ content_start = frontmatter_match.end()
+
+ # 解析YAML(简化版)
+ note = {}
+ for line in frontmatter_text.split('\n'):
+ if ':' in line:
+ key, value = line.split(':', 1)
+ key = key.strip()
+ value = value.strip()
+
+ # 处理特殊字段
+ if key == 'tags':
+ try:
+ note[key] = json.loads(value)
+ except:
+ note[key] = []
+ else:
+ note[key] = value
+
+ # 提取内容(去掉标题行)
+ markdown_content = markdown_text[content_start:].strip()
+ # 移除第一行的 # 标题
+ lines = markdown_content.split('\n')
+ if lines and lines[0].startswith('# '):
+ markdown_content = '\n'.join(lines[1:]).strip()
+
+ note['content'] = markdown_content
+
+ # 添加元数据
+ note['metadata'] = {
+ 'word_count': len(markdown_content),
+ 'status': 'active'
+ }
+
+ return note
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """执行工具"""
+ if not self.validate_parameters(parameters):
+ return "❌ 参数验证失败"
+
+ action = parameters.get("action")
+
+ if action == "create":
+ return self._create_note(parameters)
+ elif action == "read":
+ return self._read_note(parameters)
+ elif action == "update":
+ return self._update_note(parameters)
+ elif action == "delete":
+ return self._delete_note(parameters)
+ elif action == "list":
+ return self._list_notes(parameters)
+ elif action == "search":
+ return self._search_notes(parameters)
+ elif action == "summary":
+ return self._get_summary()
+ else:
+ return f"❌ 不支持的操作: {action}"
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """获取工具参数定义"""
+ return [
+ ToolParameter(
+ name="action",
+ type="string",
+ description=(
+ "操作类型: create(创建), read(读取), update(更新), "
+ "delete(删除), list(列表), search(搜索), summary(摘要)"
+ ),
+ required=True
+ ),
+ ToolParameter(
+ name="title",
+ type="string",
+ description="笔记标题(create/update时必需)",
+ required=False
+ ),
+ ToolParameter(
+ name="content",
+ type="string",
+ description="笔记内容(create/update时必需)",
+ required=False
+ ),
+ ToolParameter(
+ name="note_type",
+ type="string",
+ description=(
+ "笔记类型: task_state(任务状态), conclusion(结论), "
+ "blocker(阻塞项), action(行动计划), reference(参考), general(通用)"
+ ),
+ required=False,
+ default="general"
+ ),
+ ToolParameter(
+ name="tags",
+ type="array",
+ description="标签列表(可选)",
+ required=False
+ ),
+ ToolParameter(
+ name="note_id",
+ type="string",
+ description="笔记ID(read/update/delete时必需)",
+ required=False
+ ),
+ ToolParameter(
+ name="query",
+ type="string",
+ description="搜索关键词(search时必需)",
+ required=False
+ ),
+ ToolParameter(
+ name="limit",
+ type="integer",
+ description="返回结果数量限制(默认10)",
+ required=False,
+ default=10
+ ),
+ ]
+
+ def _create_note(self, params: Dict[str, Any]) -> str:
+ """创建笔记"""
+ title = params.get("title")
+ content = params.get("content")
+ note_type = params.get("note_type", "general")
+ tags = params.get("tags", [])
+
+ if not title or not content:
+ return "❌ 创建笔记需要提供 title 和 content"
+
+ # 检查笔记数量限制
+ if len(self.notes_index["notes"]) >= self.max_notes:
+ return f"❌ 笔记数量已达上限 ({self.max_notes})"
+
+ # 生成笔记ID
+ note_id = self._generate_note_id()
+
+ # 创建笔记对象
+ note = {
+ "id": note_id,
+ "title": title,
+ "content": content,
+ "type": note_type,
+ "tags": tags if isinstance(tags, list) else [],
+ "created_at": datetime.now().isoformat(),
+ "updated_at": datetime.now().isoformat(),
+ "metadata": {
+ "word_count": len(content),
+ "status": "active"
+ }
+ }
+
+ # 保存笔记文件(Markdown格式)
+ note_path = self._get_note_path(note_id)
+ markdown_content = self._note_to_markdown(note)
+ with open(note_path, 'w', encoding='utf-8') as f:
+ f.write(markdown_content)
+
+ # 更新索引
+ self.notes_index["notes"].append({
+ "id": note_id,
+ "title": title,
+ "type": note_type,
+ "tags": tags if isinstance(tags, list) else [],
+ "created_at": note["created_at"]
+ })
+ self.notes_index["metadata"]["total_notes"] = len(self.notes_index["notes"])
+ self._save_index()
+
+ return f"✅ 笔记创建成功\nID: {note_id}\n标题: {title}\n类型: {note_type}"
+
+ def _read_note(self, params: Dict[str, Any]) -> str:
+ """读取笔记"""
+ note_id = params.get("note_id")
+
+ if not note_id:
+ return "❌ 读取笔记需要提供 note_id"
+
+ note_path = self._get_note_path(note_id)
+ if not note_path.exists():
+ return f"❌ 笔记不存在: {note_id}"
+
+ with open(note_path, 'r', encoding='utf-8') as f:
+ markdown_text = f.read()
+
+ note = self._markdown_to_note(markdown_text)
+
+ return self._format_note(note)
+
+ def _update_note(self, params: Dict[str, Any]) -> str:
+ """更新笔记"""
+ note_id = params.get("note_id")
+
+ if not note_id:
+ return "❌ 更新笔记需要提供 note_id"
+
+ note_path = self._get_note_path(note_id)
+ if not note_path.exists():
+ return f"❌ 笔记不存在: {note_id}"
+
+ # 读取现有笔记
+ with open(note_path, 'r', encoding='utf-8') as f:
+ markdown_text = f.read()
+ note = self._markdown_to_note(markdown_text)
+
+ # 更新字段
+ if "title" in params:
+ note["title"] = params["title"]
+ if "content" in params:
+ note["content"] = params["content"]
+ note["metadata"]["word_count"] = len(params["content"])
+ if "note_type" in params:
+ note["type"] = params["note_type"]
+ if "tags" in params:
+ note["tags"] = params["tags"] if isinstance(params["tags"], list) else []
+
+ note["updated_at"] = datetime.now().isoformat()
+
+ # 保存更新(Markdown格式)
+ markdown_content = self._note_to_markdown(note)
+ with open(note_path, 'w', encoding='utf-8') as f:
+ f.write(markdown_content)
+
+ # 更新索引
+ for idx_note in self.notes_index["notes"]:
+ if idx_note["id"] == note_id:
+ idx_note["title"] = note["title"]
+ idx_note["type"] = note["type"]
+ idx_note["tags"] = note["tags"]
+ break
+ self._save_index()
+
+ return f"✅ 笔记更新成功: {note_id}"
+
+ def _delete_note(self, params: Dict[str, Any]) -> str:
+ """删除笔记"""
+ note_id = params.get("note_id")
+
+ if not note_id:
+ return "❌ 删除笔记需要提供 note_id"
+
+ note_path = self._get_note_path(note_id)
+ if not note_path.exists():
+ return f"❌ 笔记不存在: {note_id}"
+
+ # 删除文件
+ note_path.unlink()
+
+ # 更新索引
+ self.notes_index["notes"] = [
+ n for n in self.notes_index["notes"] if n["id"] != note_id
+ ]
+ self.notes_index["metadata"]["total_notes"] = len(self.notes_index["notes"])
+ self._save_index()
+
+ return f"✅ 笔记已删除: {note_id}"
+
+ def _list_notes(self, params: Dict[str, Any]) -> str:
+ """列出笔记"""
+ note_type = params.get("note_type")
+ limit = params.get("limit", 10)
+
+ # 过滤笔记
+ filtered_notes = self.notes_index["notes"]
+ if note_type:
+ filtered_notes = [n for n in filtered_notes if n["type"] == note_type]
+
+ # 限制数量
+ filtered_notes = filtered_notes[:limit]
+
+ if not filtered_notes:
+ return "📝 暂无笔记"
+
+ result = f"📝 笔记列表(共 {len(filtered_notes)} 条)\n\n"
+ for note in filtered_notes:
+ result += f"• [{note['type']}] {note['title']}\n"
+ result += f" ID: {note['id']}\n"
+ if note.get('tags'):
+ result += f" 标签: {', '.join(note['tags'])}\n"
+ result += f" 创建时间: {note['created_at']}\n\n"
+
+ return result
+
+ def _search_notes(self, params: Dict[str, Any]) -> str:
+ """搜索笔记"""
+ query = params.get("query", "").lower()
+ limit = params.get("limit", 10)
+
+ if not query:
+ return "❌ 搜索需要提供 query"
+
+ # 搜索匹配的笔记
+ matched_notes = []
+ for idx_note in self.notes_index["notes"]:
+ note_path = self._get_note_path(idx_note["id"])
+ if note_path.exists():
+ with open(note_path, 'r', encoding='utf-8') as f:
+ markdown_text = f.read()
+
+ try:
+ note = self._markdown_to_note(markdown_text)
+ except Exception as e:
+ print(f"⚠️ 解析笔记失败 {idx_note['id']}: {e}")
+ continue
+
+ # 检查标题、内容、标签是否匹配
+ if (query in note["title"].lower() or
+ query in note["content"].lower() or
+ any(query in tag.lower() for tag in note.get("tags", []))):
+ matched_notes.append(note)
+
+ # 限制数量
+ matched_notes = matched_notes[:limit]
+
+ if not matched_notes:
+ return f"📝 未找到匹配 '{query}' 的笔记"
+
+ result = f"🔍 搜索结果(共 {len(matched_notes)} 条)\n\n"
+ for note in matched_notes:
+ result += self._format_note(note, compact=True) + "\n"
+
+ return result
+
+ def _get_summary(self) -> str:
+ """获取笔记摘要"""
+ total = len(self.notes_index["notes"])
+
+ # 按类型统计
+ type_counts = {}
+ for note in self.notes_index["notes"]:
+ note_type = note["type"]
+ type_counts[note_type] = type_counts.get(note_type, 0) + 1
+
+ result = f"📊 笔记摘要\n\n"
+ result += f"总笔记数: {total}\n\n"
+ result += "按类型统计:\n"
+ for note_type, count in sorted(type_counts.items()):
+ result += f" • {note_type}: {count}\n"
+
+ return result
+
+ def _format_note(self, note: Dict[str, Any], compact: bool = False) -> str:
+ """格式化笔记输出"""
+ if compact:
+ return (
+ f"[{note['type']}] {note['title']}\n"
+ f"ID: {note['id']}\n"
+ f"内容: {note['content'][:100]}{'...' if len(note['content']) > 100 else ''}"
+ )
+ else:
+ result = f"📝 笔记详情\n\n"
+ result += f"ID: {note['id']}\n"
+ result += f"标题: {note['title']}\n"
+ result += f"类型: {note['type']}\n"
+ if note.get('tags'):
+ result += f"标签: {', '.join(note['tags'])}\n"
+ result += f"创建时间: {note['created_at']}\n"
+ result += f"更新时间: {note['updated_at']}\n"
+ result += f"\n内容:\n{note['content']}\n"
+ return result
+
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/ocr_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/ocr_tool.py
new file mode 100644
index 00000000..62a3bc93
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/ocr_tool.py
@@ -0,0 +1,95 @@
+"""OCR Tool - 图片文字提取工具
+
+当用户使用文本模型时,通过 OCR 提取图片中的文字,注入到上下文中。
+使用本地 tesseract 作为 OCR 后端。
+
+使用场景:
+- 代码截图识别
+- 报错截图提取
+- 文档图片转文字
+"""
+
+from __future__ import annotations
+
+import subprocess
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from ..base import Tool, ToolParameter
+
+
+class OCRTool(Tool):
+ """OCR 工具 - 本地 tesseract 后端"""
+
+ def __init__(self):
+ """
+ 初始化 OCR 工具
+ """
+ super().__init__(
+ name="ocr",
+ description="图片文字提取工具 - 从图片中识别并提取文字内容"
+ )
+
+ def get_parameters(self) -> List[ToolParameter]:
+ return [
+ ToolParameter(
+ name="image_path",
+ type="string",
+ description="图片文件路径",
+ required=True,
+ ),
+ ]
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """执行 OCR"""
+ image_path = parameters.get("image_path", "")
+ if not image_path:
+ return "错误:未提供图片路径"
+
+ path = Path(image_path).expanduser().resolve()
+ if not path.exists():
+ return f"错误:图片文件不存在: {path}"
+
+ # 仅使用本地 tesseract
+ result = self._ocr_via_tesseract(path)
+ if result and not result.startswith("错误"):
+ return result
+ return "OCR 失败:请安装并配置 tesseract。"
+
+ def _ocr_via_tesseract(self, image_path: Path) -> Optional[str]:
+ """通过本地 tesseract 进行 OCR"""
+ try:
+ # 检查 tesseract 是否安装
+ result = subprocess.run(
+ ["tesseract", str(image_path), "stdout", "-l", "chi_sim+eng"],
+ capture_output=True,
+ text=True,
+ timeout=30,
+ )
+ if result.returncode == 0:
+ text = result.stdout.strip()
+ if text:
+ return text
+ return "OCR 结果为空(图片中可能没有可识别的文字)"
+ return f"tesseract 错误: {result.stderr}"
+ except FileNotFoundError:
+ return None # tesseract 未安装,返回 None 让调用方知道
+ except subprocess.TimeoutExpired:
+ return "OCR 超时"
+ except Exception as e:
+ return f"本地 OCR 错误: {e}"
+
+
+def extract_text_from_image(
+ image_path: str | Path,
+) -> str:
+ """
+ 便捷函数:从图片提取文字
+
+ Args:
+ image_path: 图片路径
+ Returns:
+ 提取的文字,或错误信息
+ """
+ tool = OCRTool()
+ return tool.run({"image_path": str(image_path)})
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/plan_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/plan_tool.py
new file mode 100644
index 00000000..8a4b9995
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/plan_tool.py
@@ -0,0 +1,76 @@
+from __future__ import annotations
+
+from pathlib import Path
+from typing import Dict, Any, List, Optional
+
+from core.llm import HelloAgentsLLM
+from tools.base import Tool, ToolParameter
+
+
+class PlanTool(Tool):
+ """规划工具(可选)
+
+ 用于在用户强制要求或任务明显需要多步执行时生成计划。
+ 建议在 ReAct 中按需调用:plan[{"goal":"..."}] 或 plan[目标文本]
+ """
+
+ def __init__(self, llm: HelloAgentsLLM, prompt_path: Optional[str] = None):
+ super().__init__(name="plan", description="生成可执行计划(仅在需要时调用)")
+ self.llm = llm
+ self.prompt_path = Path(prompt_path).resolve() if prompt_path else None
+
+ def get_parameters(self) -> List[ToolParameter]:
+ return [
+ ToolParameter(
+ name="goal",
+ type="string",
+ description="计划目标(例如:分析项目结构并说明模块职责)",
+ required=True,
+ ),
+ ToolParameter(
+ name="constraints",
+ type="string",
+ description="额外约束(可选)",
+ required=False,
+ ),
+ ToolParameter(
+ name="output",
+ type="string",
+ description="输出格式:markdown|json(默认 markdown)",
+ required=False,
+ default="markdown",
+ ),
+ ]
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ if not self.validate_parameters(parameters):
+ return "❌ 参数验证失败:缺少 goal"
+
+ goal = str(parameters.get("goal", "")).strip()
+ constraints = parameters.get("constraints")
+ output = str(parameters.get("output", "markdown")).strip() or "markdown"
+
+ if not goal:
+ return "❌ goal 不能为空"
+
+ prompt = ""
+ if self.prompt_path and self.prompt_path.exists():
+ prompt = self.prompt_path.read_text(encoding="utf-8")
+ else:
+ prompt = (
+ "你是一个规划助手。请输出一个可执行计划(5~12步),并包含 Risks 与 Validation。"
+ )
+
+ user_msg = f"目标:{goal}\n期望输出:{output}"
+ if constraints:
+ user_msg += f"\n约束:{constraints}"
+
+ resp = self.llm.invoke(
+ [
+ {"role": "system", "content": prompt},
+ {"role": "user", "content": user_msg},
+ ],
+ max_tokens=800,
+ )
+ return resp or ""
+
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/protocol_tools.py b/Co-creation-projects/aug618-Praxis/tools/builtin/protocol_tools.py
new file mode 100644
index 00000000..e9fb8671
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/protocol_tools.py
@@ -0,0 +1,857 @@
+"""
+协议工具集合
+
+提供基于协议实现的工具接口:
+- MCP Tool: 基于 fastmcp 库,用于连接和调用 MCP 服务器
+- A2A Tool: 基于官方 a2a 库,用于 Agent 间通信(需要安装 a2a)
+- ANP Tool: 基于概念实现,用于服务发现和网络管理
+"""
+
+from typing import Dict, Any, List, Optional
+from ..base import Tool, ToolParameter
+from utils.env import env_flag
+import os
+
+
+# MCP服务器环境变量映射表
+# 用于自动检测常见MCP服务器需要的环境变量
+MCP_SERVER_ENV_MAP = {
+ "server-github": ["GITHUB_PERSONAL_ACCESS_TOKEN"],
+ "server-slack": ["SLACK_BOT_TOKEN", "SLACK_TEAM_ID"],
+ "server-google-drive": ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GOOGLE_REFRESH_TOKEN"],
+ "server-postgres": ["POSTGRES_CONNECTION_STRING"],
+ "server-sqlite": [], # 不需要环境变量
+ "server-filesystem": [], # 不需要环境变量
+}
+
+
+class MCPTool(Tool):
+ """MCP (Model Context Protocol) 工具
+
+ 连接到 MCP 服务器并调用其提供的工具、资源和提示词。
+
+ 功能:
+ - 列出服务器提供的工具
+ - 调用服务器工具
+ - 读取服务器资源
+ - 获取提示词模板
+
+ 使用示例:
+ >>> from hello_agents.tools.builtin import MCPTool
+ >>>
+ >>> # 方式1: 使用内置演示服务器
+ >>> tool = MCPTool() # 自动创建内置服务器
+ >>> result = tool.run({"action": "list_tools"})
+ >>>
+ >>> # 方式2: 连接到外部 MCP 服务器
+ >>> tool = MCPTool(server_command=["python", "examples/mcp_example.py"])
+ >>> result = tool.run({"action": "list_tools"})
+ >>>
+ >>> # 方式3: 使用自定义 FastMCP 服务器
+ >>> from fastmcp import FastMCP
+ >>> server = FastMCP("MyServer")
+ >>> tool = MCPTool(server=server)
+
+ 注意:使用 fastmcp 库,已包含在依赖中
+ """
+
+ def __init__(self,
+ name: str = "mcp",
+ description: Optional[str] = None,
+ server_command: Optional[List[str]] = None,
+ server_args: Optional[List[str]] = None,
+ server: Optional[Any] = None,
+ auto_expand: bool = True,
+ env: Optional[Dict[str, str]] = None,
+ env_keys: Optional[List[str]] = None):
+ """
+ 初始化 MCP 工具
+
+ Args:
+ name: 工具名称(默认为"mcp",建议为不同服务器指定不同名称)
+ description: 工具描述(可选,默认为通用描述)
+ server_command: 服务器启动命令(如 ["python", "server.py"])
+ server_args: 服务器参数列表
+ server: FastMCP 服务器实例(可选,用于内存传输)
+ auto_expand: 是否自动展开为独立工具(默认True)
+ env: 环境变量字典(优先级最高,直接传递给MCP服务器)
+ env_keys: 要从系统环境变量加载的key列表(优先级中等)
+
+ 环境变量优先级(从高到低):
+ 1. 直接传递的env参数
+ 2. env_keys指定的环境变量
+ 3. 自动检测的环境变量(根据server_command)
+
+ 注意:如果所有参数都为空,将创建内置演示服务器
+
+ 示例:
+ >>> # 方式1:直接传递环境变量(优先级最高)
+ >>> github_tool = MCPTool(
+ ... name="github",
+ ... server_command=["npx", "-y", "@modelcontextprotocol/server-github"],
+ ... env={"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx"}
+ ... )
+ >>>
+ >>> # 方式2:从.env文件加载指定的环境变量
+ >>> github_tool = MCPTool(
+ ... name="github",
+ ... server_command=["npx", "-y", "@modelcontextprotocol/server-github"],
+ ... env_keys=["GITHUB_PERSONAL_ACCESS_TOKEN"]
+ ... )
+ >>>
+ >>> # 方式3:自动检测(最简单,推荐)
+ >>> github_tool = MCPTool(
+ ... name="github",
+ ... server_command=["npx", "-y", "@modelcontextprotocol/server-github"]
+ ... # 自动从环境变量加载GITHUB_PERSONAL_ACCESS_TOKEN
+ ... )
+ """
+ self.server_command = server_command
+ self.server_args = server_args or []
+ self.server = server
+ self._client = None
+ self._available_tools = []
+ self.auto_expand = auto_expand
+ self.prefix = f"{name}_" if auto_expand else ""
+
+ # 环境变量处理(优先级:env > env_keys > 自动检测)
+ self.env = self._prepare_env(env, env_keys, server_command)
+
+ # 如果没有指定任何服务器,创建内置演示服务器
+ if not server_command and not server:
+ self.server = self._create_builtin_server()
+
+ # 自动发现工具
+ self._discover_tools()
+
+ # 设置默认描述或自动生成
+ if description is None:
+ description = self._generate_description()
+
+ super().__init__(
+ name=name,
+ description=description
+ )
+
+ def _prepare_env(self,
+ env: Optional[Dict[str, str]],
+ env_keys: Optional[List[str]],
+ server_command: Optional[List[str]]) -> Dict[str, str]:
+ """
+ 准备环境变量
+
+ 优先级:env > env_keys > 自动检测
+
+ Args:
+ env: 直接传递的环境变量字典
+ env_keys: 要从系统环境变量加载的key列表
+ server_command: 服务器命令(用于自动检测)
+
+ Returns:
+ 合并后的环境变量字典
+ """
+ result_env = {}
+ quiet = env_flag("CODE_AGENT_QUIET", default=False)
+
+ # 1. 自动检测(优先级最低)
+ if server_command:
+ # 从命令中提取服务器名称
+ server_name = None
+ for part in server_command:
+ if "server-" in part:
+ # 提取类似 "@modelcontextprotocol/server-github" 中的 "server-github"
+ server_name = part.split("/")[-1] if "/" in part else part
+ break
+
+ # 查找映射表
+ if server_name and server_name in MCP_SERVER_ENV_MAP:
+ auto_keys = MCP_SERVER_ENV_MAP[server_name]
+ for key in auto_keys:
+ value = os.getenv(key)
+ if value:
+ result_env[key] = value
+ if not quiet:
+ print(f"🔑 自动加载环境变量: {key}")
+
+ # 2. env_keys指定的环境变量(优先级中等)
+ if env_keys:
+ for key in env_keys:
+ value = os.getenv(key)
+ if value:
+ result_env[key] = value
+ if not quiet:
+ print(f"🔑 从env_keys加载环境变量: {key}")
+ else:
+ if not quiet:
+ print(f"⚠️ 警告: 环境变量 {key} 未设置")
+
+ # 3. 直接传递的env(优先级最高)
+ if env:
+ result_env.update(env)
+ for key in env.keys():
+ if not quiet:
+ print(f"🔑 使用直接传递的环境变量: {key}")
+
+ return result_env
+
+ def _create_builtin_server(self):
+ """创建内置演示服务器"""
+ try:
+ from fastmcp import FastMCP
+
+ server = FastMCP("HelloAgents-BuiltinServer")
+
+ @server.tool()
+ def add(a: float, b: float) -> float:
+ """加法计算器"""
+ return a + b
+
+ @server.tool()
+ def subtract(a: float, b: float) -> float:
+ """减法计算器"""
+ return a - b
+
+ @server.tool()
+ def multiply(a: float, b: float) -> float:
+ """乘法计算器"""
+ return a * b
+
+ @server.tool()
+ def divide(a: float, b: float) -> float:
+ """除法计算器"""
+ if b == 0:
+ raise ValueError("除数不能为零")
+ return a / b
+
+ @server.tool()
+ def greet(name: str = "World") -> str:
+ """友好问候"""
+ return f"Hello, {name}! 欢迎使用 HelloAgents MCP 工具!"
+
+ @server.tool()
+ def get_system_info() -> dict:
+ """获取系统信息"""
+ import platform
+ import sys
+ return {
+ "platform": platform.system(),
+ "python_version": sys.version,
+ "server_name": "HelloAgents-BuiltinServer",
+ "tools_count": 6
+ }
+
+ return server
+
+ except ImportError:
+ raise ImportError(
+ "创建内置 MCP 服务器需要 fastmcp 库。请安装: pip install fastmcp"
+ )
+
+ def _discover_tools(self):
+ """发现MCP服务器提供的所有工具"""
+ try:
+ from hello_agents.protocols.mcp.client import MCPClient
+ import asyncio
+
+ async def discover():
+ client_source = self.server if self.server else self.server_command
+ async with MCPClient(client_source, self.server_args, env=self.env) as client:
+ tools = await client.list_tools()
+ return tools
+
+ # 运行异步发现
+ try:
+ loop = asyncio.get_running_loop()
+ # 如果已有循环,在新线程中运行
+ import concurrent.futures
+ def run_in_thread():
+ new_loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(new_loop)
+ try:
+ return new_loop.run_until_complete(discover())
+ finally:
+ new_loop.close()
+
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ future = executor.submit(run_in_thread)
+ self._available_tools = future.result()
+ except RuntimeError:
+ # 没有运行中的循环
+ self._available_tools = asyncio.run(discover())
+
+ except Exception as e:
+ # 工具发现失败不影响初始化
+ self._available_tools = []
+
+ def _generate_description(self) -> str:
+ """生成增强的工具描述"""
+ if not self._available_tools:
+ return "连接到 MCP 服务器,调用工具、读取资源和获取提示词。支持内置服务器和外部服务器。"
+
+ if self.auto_expand:
+ # 展开模式:简单描述
+ return f"MCP工具服务器,包含{len(self._available_tools)}个工具。这些工具会自动展开为独立的工具供Agent使用。"
+ else:
+ # 非展开模式:详细描述
+ desc_parts = [
+ f"MCP工具服务器,提供{len(self._available_tools)}个工具:"
+ ]
+
+ # 列出所有工具
+ for tool in self._available_tools:
+ tool_name = tool.get('name', 'unknown')
+ tool_desc = tool.get('description', '无描述')
+ # 简化描述,只取第一句
+ short_desc = tool_desc.split('.')[0] if tool_desc else '无描述'
+ desc_parts.append(f" • {tool_name}: {short_desc}")
+
+ # 添加调用格式说明
+ desc_parts.append("\n调用格式:返回JSON格式的参数")
+ desc_parts.append('{"action": "call_tool", "tool_name": "工具名", "arguments": {...}}')
+
+ # 添加示例
+ if self._available_tools:
+ first_tool = self._available_tools[0]
+ tool_name = first_tool.get('name', 'example')
+ desc_parts.append(f'\n示例:{{"action": "call_tool", "tool_name": "{tool_name}", "arguments": {{...}}}}')
+
+ return "\n".join(desc_parts)
+
+ def get_expanded_tools(self) -> List['Tool']: # type: ignore
+ """
+ 获取展开的工具列表
+
+ 将MCP服务器的每个工具包装成独立的Tool对象
+
+ Returns:
+ Tool对象列表
+ """
+ if not self.auto_expand:
+ return []
+
+ from .mcp_wrapper_tool import MCPWrappedTool
+
+ expanded_tools = []
+ for tool_info in self._available_tools:
+ wrapped_tool = MCPWrappedTool(
+ mcp_tool=self,
+ tool_info=tool_info,
+ prefix=self.prefix
+ )
+ expanded_tools.append(wrapped_tool)
+
+ return expanded_tools
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """
+ 执行 MCP 操作
+
+ Args:
+ parameters: 包含以下参数的字典
+ - action: 操作类型 (list_tools, call_tool, list_resources, read_resource, list_prompts, get_prompt)
+ 如果不指定action但指定了tool_name,会自动推断为call_tool
+ - tool_name: 工具名称(call_tool 需要)
+ - arguments: 工具参数(call_tool 需要)
+ - uri: 资源 URI(read_resource 需要)
+ - prompt_name: 提示词名称(get_prompt 需要)
+ - prompt_arguments: 提示词参数(get_prompt 可选)
+
+ Returns:
+ 操作结果
+ """
+ from hello_agents.protocols.mcp.client import MCPClient
+
+ # 智能推断action:如果没有action但有tool_name,自动设置为call_tool
+ action = parameters.get("action", "").lower()
+ if not action and "tool_name" in parameters:
+ action = "call_tool"
+ parameters["action"] = action
+
+ if not action:
+ return "错误:必须指定 action 参数或 tool_name 参数"
+
+ try:
+ # 使用增强的异步客户端
+ import asyncio
+ from hello_agents.protocols.mcp.client import MCPClient
+
+ async def run_mcp_operation():
+ # 根据配置选择客户端创建方式
+ if self.server:
+ # 使用内置服务器(内存传输)
+ client_source = self.server
+ else:
+ # 使用外部服务器命令
+ client_source = self.server_command
+
+ async with MCPClient(client_source, self.server_args, env=self.env) as client:
+ if action == "list_tools":
+ tools = await client.list_tools()
+ if not tools:
+ return "没有找到可用的工具"
+ result = f"找到 {len(tools)} 个工具:\n"
+ for tool in tools:
+ result += f"- {tool['name']}: {tool['description']}\n"
+ return result
+
+ elif action == "call_tool":
+ tool_name = parameters.get("tool_name")
+ arguments = parameters.get("arguments", {})
+ if not tool_name:
+ return "错误:必须指定 tool_name 参数"
+ result = await client.call_tool(tool_name, arguments)
+ return f"工具 '{tool_name}' 执行结果:\n{result}"
+
+ elif action == "list_resources":
+ resources = await client.list_resources()
+ if not resources:
+ return "没有找到可用的资源"
+ result = f"找到 {len(resources)} 个资源:\n"
+ for resource in resources:
+ result += f"- {resource['uri']}: {resource['name']}\n"
+ return result
+
+ elif action == "read_resource":
+ uri = parameters.get("uri")
+ if not uri:
+ return "错误:必须指定 uri 参数"
+ content = await client.read_resource(uri)
+ return f"资源 '{uri}' 内容:\n{content}"
+
+ elif action == "list_prompts":
+ prompts = await client.list_prompts()
+ if not prompts:
+ return "没有找到可用的提示词"
+ result = f"找到 {len(prompts)} 个提示词:\n"
+ for prompt in prompts:
+ result += f"- {prompt['name']}: {prompt['description']}\n"
+ return result
+
+ elif action == "get_prompt":
+ prompt_name = parameters.get("prompt_name")
+ prompt_arguments = parameters.get("prompt_arguments", {})
+ if not prompt_name:
+ return "错误:必须指定 prompt_name 参数"
+ messages = await client.get_prompt(prompt_name, prompt_arguments)
+ result = f"提示词 '{prompt_name}':\n"
+ for msg in messages:
+ result += f"[{msg['role']}] {msg['content']}\n"
+ return result
+
+ else:
+ return f"错误:不支持的操作 '{action}'"
+
+ # 运行异步操作
+ try:
+ # 检查是否已有运行中的事件循环
+ try:
+ loop = asyncio.get_running_loop()
+ # 如果有运行中的循环,在新线程中运行新的事件循环
+ import concurrent.futures
+ import threading
+
+ def run_in_thread():
+ # 在新线程中创建新的事件循环
+ new_loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(new_loop)
+ try:
+ return new_loop.run_until_complete(run_mcp_operation())
+ finally:
+ new_loop.close()
+
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ future = executor.submit(run_in_thread)
+ return future.result()
+ except RuntimeError:
+ # 没有运行中的循环,直接运行
+ return asyncio.run(run_mcp_operation())
+ except Exception as e:
+ return f"异步操作失败: {str(e)}"
+
+ except Exception as e:
+ return f"MCP 操作失败: {str(e)}"
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """获取工具参数定义"""
+ return [
+ ToolParameter(
+ name="action",
+ type="string",
+ description="操作类型: list_tools, call_tool, list_resources, read_resource, list_prompts, get_prompt",
+ required=True
+ ),
+ ToolParameter(
+ name="tool_name",
+ type="string",
+ description="工具名称(call_tool 操作需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="arguments",
+ type="object",
+ description="工具参数(call_tool 操作需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="uri",
+ type="string",
+ description="资源 URI(read_resource 操作需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="prompt_name",
+ type="string",
+ description="提示词名称(get_prompt 操作需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="prompt_arguments",
+ type="object",
+ description="提示词参数(get_prompt 操作可选)",
+ required=False
+ )
+ ]
+
+
+class A2ATool(Tool):
+ """A2A (Agent-to-Agent Protocol) 工具
+
+ 连接到 A2A Agent 并进行通信。
+
+ 功能:
+ - 向 Agent 提问
+ - 获取 Agent 信息
+ - 发送自定义消息
+
+ 使用示例:
+ >>> from hello_agents.tools.builtin import A2ATool
+ >>> # 连接到 A2A Agent(使用默认名称)
+ >>> tool = A2ATool(agent_url="http://localhost:5000")
+ >>> # 连接到 A2A Agent(自定义名称和描述)
+ >>> tool = A2ATool(
+ ... agent_url="http://localhost:5000",
+ ... name="tech_expert",
+ ... description="技术专家,回答技术相关问题"
+ ... )
+ >>> # 提问
+ >>> result = tool.run({"action": "ask", "question": "计算 2+2"})
+ >>> # 获取信息
+ >>> result = tool.run({"action": "get_info"})
+
+ 注意:需要安装官方 a2a-sdk 库: pip install a2a-sdk
+ 详见文档: docs/chapter10/A2A_GUIDE.md
+ 官方仓库: https://github.com/a2aproject/a2a-python
+ """
+
+ def __init__(self, agent_url: str, name: str = "a2a", description: str = None):
+ """
+ 初始化 A2A 工具
+
+ Args:
+ agent_url: Agent URL
+ name: 工具名称(可选,默认为 "a2a")
+ description: 工具描述(可选)
+ """
+ if description is None:
+ description = "连接到 A2A Agent,支持提问和获取信息。需要安装官方 a2a-sdk 库。"
+
+ super().__init__(
+ name=name,
+ description=description
+ )
+ self.agent_url = agent_url
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """
+ 执行 A2A 操作
+
+ Args:
+ parameters: 包含以下参数的字典
+ - action: 操作类型 (ask, get_info)
+ - question: 问题文本(ask 需要)
+
+ Returns:
+ 操作结果
+ """
+ try:
+ from hello_agents.protocols.a2a.implementation import A2AClient, A2A_AVAILABLE
+ if not A2A_AVAILABLE:
+ return ("错误:需要安装 a2a-sdk 库\n"
+ "安装命令: pip install a2a-sdk\n"
+ "详见文档: docs/chapter10/A2A_GUIDE.md\n"
+ "官方仓库: https://github.com/a2aproject/a2a-python")
+ except ImportError:
+ return ("错误:无法导入 A2A 模块\n"
+ "安装命令: pip install a2a-sdk\n"
+ "详见文档: docs/chapter10/A2A_GUIDE.md\n"
+ "官方仓库: https://github.com/a2aproject/a2a-python")
+
+ action = parameters.get("action", "").lower()
+
+ if not action:
+ return "错误:必须指定 action 参数"
+
+ try:
+ client = A2AClient(self.agent_url)
+
+ if action == "ask":
+ question = parameters.get("question")
+ if not question:
+ return "错误:必须指定 question 参数"
+ response = client.ask(question)
+ return f"Agent 回答:\n{response}"
+
+ elif action == "get_info":
+ info = client.get_info()
+ result = "Agent 信息:\n"
+ for key, value in info.items():
+ result += f"- {key}: {value}\n"
+ return result
+
+ else:
+ return f"错误:不支持的操作 '{action}'"
+
+ except Exception as e:
+ return f"A2A 操作失败: {str(e)}"
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """获取工具参数定义"""
+ return [
+ ToolParameter(
+ name="action",
+ type="string",
+ description="操作类型: ask(提问), get_info(获取信息)",
+ required=True
+ ),
+ ToolParameter(
+ name="question",
+ type="string",
+ description="问题文本(ask 操作需要)",
+ required=False
+ )
+ ]
+
+
+class ANPTool(Tool):
+ """ANP (Agent Network Protocol) 工具
+
+ 提供智能体网络管理功能,包括服务发现、节点管理和消息路由。
+ 这是一个概念性实现,用于演示 Agent 网络管理的核心理念。
+
+ 功能:
+ - 注册和发现服务
+ - 添加和管理网络节点
+ - 消息路由
+ - 网络统计
+
+ 使用示例:
+ >>> from hello_agents.tools.builtin import ANPTool
+ >>> tool = ANPTool()
+ >>> # 注册服务
+ >>> result = tool.run({
+ ... "action": "register_service",
+ ... "service_id": "calc-1",
+ ... "service_type": "calculator",
+ ... "endpoint": "http://localhost:5001"
+ ... })
+ >>> # 发现服务
+ >>> result = tool.run({
+ ... "action": "discover_services",
+ ... "service_type": "calculator"
+ ... })
+ >>> # 添加节点
+ >>> result = tool.run({
+ ... "action": "add_node",
+ ... "node_id": "agent-1",
+ ... "endpoint": "http://localhost:5001"
+ ... })
+
+ 注意:这是概念性实现,不需要额外依赖
+ 详见文档: docs/chapter10/ANP_CONCEPTS.md
+ """
+
+ def __init__(self, name: str = "anp", description: str = None, discovery=None, network=None):
+ """初始化 ANP 工具
+
+ Args:
+ name: 工具名称
+ description: 工具描述
+ discovery: 可选的 ANPDiscovery 实例,如果不提供则创建新实例
+ network: 可选的 ANPNetwork 实例,如果不提供则创建新实例
+ """
+ if description is None:
+ description = "智能体网络管理工具,支持服务发现、节点管理和消息路由。概念性实现。"
+
+ super().__init__(
+ name=name,
+ description=description
+ )
+ from hello_agents.protocols.anp.implementation import ANPDiscovery, ANPNetwork
+ self._discovery = discovery if discovery is not None else ANPDiscovery()
+ self._network = network if network is not None else ANPNetwork()
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """
+ 执行 ANP 操作
+
+ Args:
+ parameters: 包含以下参数的字典
+ - action: 操作类型 (register_service, discover_services, add_node, route_message, get_stats)
+ - service_id, service_type, endpoint: 服务信息(register_service 需要)
+ - node_id, endpoint: 节点信息(add_node 需要)
+ - from_node, to_node, message: 路由信息(route_message 需要)
+
+ Returns:
+ 操作结果
+ """
+ from hello_agents.protocols.anp.implementation import ServiceInfo
+
+ action = parameters.get("action", "").lower()
+
+ if not action:
+ return "错误:必须指定 action 参数"
+
+ try:
+ if action == "register_service":
+ service_id = parameters.get("service_id")
+ service_type = parameters.get("service_type")
+ endpoint = parameters.get("endpoint")
+ metadata = parameters.get("metadata", {})
+
+ if not all([service_id, service_type, endpoint]):
+ return "错误:必须指定 service_id, service_type 和 endpoint 参数"
+
+ service = ServiceInfo(service_id, service_type, endpoint, metadata)
+ self._discovery.register_service(service)
+ return f"✅ 已注册服务 '{service_id}'"
+
+ elif action == "unregister_service":
+ service_id = parameters.get("service_id")
+ if not service_id:
+ return "错误:必须指定 service_id 参数"
+
+ # 使用 ANPDiscovery 的 unregister_service 方法
+ success = self._discovery.unregister_service(service_id)
+
+ if success:
+ return f"✅ 已注销服务 '{service_id}'"
+ else:
+ return f"错误:服务 '{service_id}' 不存在"
+
+ elif action == "discover_services":
+ service_type = parameters.get("service_type")
+ services = self._discovery.discover_services(service_type)
+
+ if not services:
+ return "没有找到服务"
+
+ result = f"找到 {len(services)} 个服务:\n\n"
+ for service in services:
+ result += f"服务ID: {service.service_id}\n"
+ result += f" 名称: {service.service_name}\n"
+ result += f" 类型: {service.service_type}\n"
+ result += f" 端点: {service.endpoint}\n"
+ if service.capabilities:
+ result += f" 能力: {', '.join(service.capabilities)}\n"
+ if service.metadata:
+ result += f" 元数据: {service.metadata}\n"
+ result += "\n"
+ return result
+
+ elif action == "add_node":
+ node_id = parameters.get("node_id")
+ endpoint = parameters.get("endpoint")
+ metadata = parameters.get("metadata", {})
+
+ if not all([node_id, endpoint]):
+ return "错误:必须指定 node_id 和 endpoint 参数"
+
+ self._network.add_node(node_id, endpoint, metadata)
+ return f"✅ 已添加节点 '{node_id}'"
+
+ elif action == "route_message":
+ from_node = parameters.get("from_node")
+ to_node = parameters.get("to_node")
+ message = parameters.get("message", {})
+
+ if not all([from_node, to_node]):
+ return "错误:必须指定 from_node 和 to_node 参数"
+
+ path = self._network.route_message(from_node, to_node, message)
+ if path:
+ return f"消息路由路径: {' -> '.join(path)}"
+ else:
+ return "无法找到路由路径"
+
+ elif action == "get_stats":
+ stats = self._network.get_network_stats()
+ result = "网络统计:\n"
+ for key, value in stats.items():
+ result += f"- {key}: {value}\n"
+ return result
+
+ else:
+ return f"错误:不支持的操作 '{action}'"
+
+ except Exception as e:
+ return f"ANP 操作失败: {str(e)}"
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """获取工具参数定义"""
+ return [
+ ToolParameter(
+ name="action",
+ type="string",
+ description="操作类型: register_service, unregister_service, discover_services, add_node, route_message, get_stats",
+ required=True
+ ),
+ ToolParameter(
+ name="service_id",
+ type="string",
+ description="服务 ID(register_service, unregister_service 需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="service_type",
+ type="string",
+ description="服务类型(register_service 需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="endpoint",
+ type="string",
+ description="端点地址(register_service, add_node 需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="node_id",
+ type="string",
+ description="节点 ID(add_node 需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="from_node",
+ type="string",
+ description="源节点 ID(route_message 需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="to_node",
+ type="string",
+ description="目标节点 ID(route_message 需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="message",
+ type="object",
+ description="消息内容(route_message 需要)",
+ required=False
+ ),
+ ToolParameter(
+ name="metadata",
+ type="object",
+ description="元数据(register_service, add_node 可选)",
+ required=False
+ )
+ ]
+
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/search.py b/Co-creation-projects/aug618-Praxis/tools/builtin/search.py
new file mode 100644
index 00000000..0a200031
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/search.py
@@ -0,0 +1,264 @@
+"""搜索工具 - HelloAgents原生搜索实现"""
+
+import os
+from typing import Optional, Dict, Any, List
+
+from ..base import Tool, ToolParameter
+
+class SearchTool(Tool):
+ """
+ 智能混合搜索工具
+
+ 支持多种搜索引擎后端,智能选择最佳搜索源:
+ 1. 混合模式 (hybrid) - 智能选择TAVILY或SERPAPI
+ 2. Tavily API (tavily) - 专业AI搜索
+ 3. SerpApi (serpapi) - 传统Google搜索
+ """
+
+ def __init__(self, backend: str = "hybrid", tavily_key: Optional[str] = None, serpapi_key: Optional[str] = None):
+ super().__init__(
+ name="search",
+ description="一个智能网页搜索引擎。支持混合搜索模式,自动选择最佳搜索源。当你需要回答关于时事、事实以及在你的知识库中找不到的信息时,应使用此工具。"
+ )
+ self.backend = backend
+ self.tavily_key = tavily_key or os.getenv("TAVILY_API_KEY")
+ self.serpapi_key = serpapi_key or os.getenv("SERPAPI_API_KEY")
+ self.available_backends = []
+ self._setup_backends()
+
+ def _setup_backends(self):
+ """设置搜索后端"""
+ # 检查Tavily可用性
+ if self.tavily_key:
+ try:
+ from tavily import TavilyClient
+ self.tavily_client = TavilyClient(api_key=self.tavily_key)
+ self.available_backends.append("tavily")
+ print("✅ Tavily搜索引擎已初始化")
+ except ImportError:
+ print("⚠️ Tavily未安装,无法使用Tavily搜索")
+ else:
+ print("⚠️ TAVILY_API_KEY未设置")
+
+ # 检查SerpApi可用性
+ if self.serpapi_key:
+ try:
+ import serpapi
+ self.available_backends.append("serpapi")
+ print("✅ SerpApi搜索引擎已初始化")
+ except ImportError:
+ print("⚠️ SerpApi未安装,无法使用SerpApi搜索")
+ else:
+ print("⚠️ SERPAPI_API_KEY未设置")
+
+ # 确定最终使用的后端
+ if self.backend == "hybrid":
+ if self.available_backends:
+ print(f"🔧 混合搜索模式已启用,可用后端: {', '.join(self.available_backends)}")
+ else:
+ print("⚠️ 没有可用的搜索后端,请配置API密钥")
+ elif self.backend == "tavily" and "tavily" not in self.available_backends:
+ print("⚠️ Tavily不可用,请检查TAVILY_API_KEY配置")
+ elif self.backend == "serpapi" and "serpapi" not in self.available_backends:
+ print("⚠️ SerpApi不可用,请检查SERPAPI_API_KEY配置")
+ elif self.backend not in ["tavily", "serpapi", "hybrid"]:
+ print("⚠️ 不支持的搜索后端,将使用hybrid模式")
+ self.backend = "hybrid"
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """
+ 执行搜索
+
+ Args:
+ parameters: 包含input参数的字典
+
+ Returns:
+ 搜索结果
+ """
+ query = parameters.get("input", "").strip()
+ if not query:
+ return "错误:搜索查询不能为空"
+
+ print(f"🔍 正在执行搜索: {query}")
+
+ try:
+ if self.backend == "hybrid":
+ return self._search_hybrid(query)
+ elif self.backend == "tavily":
+ if "tavily" not in self.available_backends:
+ return self._get_api_config_message()
+ return self._search_tavily(query)
+ elif self.backend == "serpapi":
+ if "serpapi" not in self.available_backends:
+ return self._get_api_config_message()
+ return self._search_serpapi(query)
+ else:
+ return self._get_api_config_message()
+ except Exception as e:
+ return f"搜索时发生错误: {str(e)}"
+
+ def _search_hybrid(self, query: str) -> str:
+ """混合搜索 - 智能选择最佳搜索源"""
+ # 检查是否有可用的搜索源
+ if not self.available_backends:
+ return self._get_api_config_message()
+
+ # 优先使用Tavily(AI优化的搜索)
+ if "tavily" in self.available_backends:
+ try:
+ print("🎯 使用Tavily进行AI优化搜索")
+ return self._search_tavily(query)
+ except Exception as e:
+ print(f"⚠️ Tavily搜索失败: {e}")
+ # 如果Tavily失败,尝试SerpApi
+ if "serpapi" in self.available_backends:
+ print("🔄 切换到SerpApi搜索")
+ return self._search_serpapi(query)
+
+ # 如果Tavily不可用,使用SerpApi
+ elif "serpapi" in self.available_backends:
+ try:
+ print("🎯 使用SerpApi进行Google搜索")
+ return self._search_serpapi(query)
+ except Exception as e:
+ print(f"⚠️ SerpApi搜索失败: {e}")
+
+ # 如果都失败了,返回API配置提示
+ return "❌ 所有搜索源都失败了,请检查网络连接和API密钥配置"
+
+ def _search_tavily(self, query: str) -> str:
+ """使用Tavily搜索"""
+ response = self.tavily_client.search(
+ query=query,
+ search_depth="basic",
+ include_answer=True,
+ max_results=3
+ )
+
+ result = f"🎯 Tavily AI搜索结果:{response.get('answer', '未找到直接答案')}\n\n"
+
+ for i, item in enumerate(response.get('results', [])[:3], 1):
+ result += f"[{i}] {item.get('title', '')}\n"
+ result += f" {item.get('content', '')[:200]}...\n"
+ result += f" 来源: {item.get('url', '')}\n\n"
+
+ return result
+
+ def _search_serpapi(self, query: str) -> str:
+ """使用SerpApi搜索"""
+ try:
+ from serpapi import SerpApiClient
+ except ImportError:
+ return "错误:SerpApi未安装,请运行 pip install serpapi"
+
+ params = {
+ "engine": "google",
+ "q": query,
+ "api_key": self.serpapi_key,
+ "gl": "cn",
+ "hl": "zh-cn",
+ }
+
+ client = SerpApiClient(params)
+ results = client.get_dict()
+
+ result_text = "🔍 SerpApi Google搜索结果:\n\n"
+
+ # 智能解析:优先寻找最直接的答案
+ if "answer_box" in results and "answer" in results["answer_box"]:
+ result_text += f"💡 直接答案:{results['answer_box']['answer']}\n\n"
+
+ if "knowledge_graph" in results and "description" in results["knowledge_graph"]:
+ result_text += f"📖 知识图谱:{results['knowledge_graph']['description']}\n\n"
+
+ if "organic_results" in results and results["organic_results"]:
+ result_text += "🔗 相关结果:\n"
+ for i, res in enumerate(results["organic_results"][:3], 1):
+ result_text += f"[{i}] {res.get('title', '')}\n"
+ result_text += f" {res.get('snippet', '')}\n"
+ result_text += f" 来源: {res.get('link', '')}\n\n"
+ return result_text
+
+ return f"对不起,没有找到关于 '{query}' 的信息。"
+
+ def _get_api_config_message(self) -> str:
+ """获取API配置提示信息"""
+ tavily_key = os.getenv("TAVILY_API_KEY")
+ serpapi_key = os.getenv("SERPAPI_API_KEY")
+
+ message = "❌ 没有可用的搜索源,请检查以下配置:\n\n"
+
+ # 检查Tavily
+ message += "1. Tavily API:\n"
+ if not tavily_key:
+ message += " ❌ 环境变量 TAVILY_API_KEY 未设置\n"
+ message += " 📝 获取地址: https://tavily.com/\n"
+ else:
+ try:
+ import tavily
+ message += " ✅ API密钥已配置,包已安装\n"
+ except ImportError:
+ message += " ❌ API密钥已配置,但需要安装包: pip install tavily-python\n"
+
+ message += "\n"
+
+ # 检查SerpAPI
+ message += "2. SerpAPI:\n"
+ if not serpapi_key:
+ message += " ❌ 环境变量 SERPAPI_API_KEY 未设置\n"
+ message += " 📝 获取地址: https://serpapi.com/\n"
+ else:
+ try:
+ import serpapi
+ message += " ✅ API密钥已配置,包已安装\n"
+ except ImportError:
+ message += " ❌ API密钥已配置,但需要安装包: pip install google-search-results\n"
+
+ message += "\n配置方法:\n"
+ message += "- 在.env文件中添加: TAVILY_API_KEY=your_key_here\n"
+ message += "- 或在环境变量中设置: export TAVILY_API_KEY=your_key_here\n"
+ message += "\n配置后重新运行程序。"
+
+ return message
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """获取工具参数定义"""
+ return [
+ ToolParameter(
+ name="input",
+ type="string",
+ description="搜索查询关键词",
+ required=True
+ )
+ ]
+
+# 便捷函数
+def search(query: str, backend: str = "hybrid") -> str:
+ """
+ 便捷的搜索函数
+
+ Args:
+ query: 搜索查询关键词
+ backend: 搜索后端 ("hybrid", "tavily", "serpapi")
+
+ Returns:
+ 搜索结果
+ """
+ tool = SearchTool(backend=backend)
+ return tool.run({"input": query})
+
+# 专用搜索函数
+def search_tavily(query: str) -> str:
+ """使用Tavily进行AI优化搜索"""
+ tool = SearchTool(backend="tavily")
+ return tool.run({"input": query})
+
+def search_serpapi(query: str) -> str:
+ """使用SerpApi进行Google搜索"""
+ tool = SearchTool(backend="serpapi")
+ return tool.run({"input": query})
+
+def search_hybrid(query: str) -> str:
+ """智能混合搜索,自动选择最佳搜索源"""
+ tool = SearchTool(backend="hybrid")
+ return tool.run({"input": query})
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/skills_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/skills_tool.py
new file mode 100644
index 00000000..6898c44f
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/skills_tool.py
@@ -0,0 +1,250 @@
+"""Skills tool - load and query installed agent skills.
+
+This integrates "skills" (SOP / playbooks / procedural knowledge) into the agent
+as *progressively disclosed* context: the model can list/search skills, then
+load the full SKILL.md only when needed.
+
+Expected directory layout (Claude-style):
+ .agents/skills//SKILL.md
+Where SKILL.md may contain YAML front matter with fields like:
+ ---
+ name: find-skills
+ description: ...
+ ---
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from ..base import Tool, ToolParameter
+from utils.env import env_stripped
+
+
+@dataclass(frozen=True)
+class SkillMeta:
+ skill_id: str
+ path: Path
+ name: str
+ description: str
+
+
+def _parse_front_matter(md: str) -> tuple[dict[str, str], str]:
+ """Parse a very small YAML front matter subset (key: value)."""
+ text = md or ""
+ lines = text.splitlines()
+ if len(lines) < 3 or lines[0].strip() != "---":
+ return {}, text
+ # find closing ---
+ end = None
+ for i in range(1, min(len(lines), 80)):
+ if lines[i].strip() == "---":
+ end = i
+ break
+ if end is None:
+ return {}, text
+ meta_lines = lines[1:end]
+ meta: dict[str, str] = {}
+ for line in meta_lines:
+ if ":" not in line:
+ continue
+ k, v = line.split(":", 1)
+ k = k.strip()
+ v = v.strip().strip('"').strip("'")
+ if k:
+ meta[k] = v
+ body = "\n".join(lines[end + 1 :]).lstrip("\n")
+ return meta, body
+
+
+class SkillsTool(Tool):
+ """Manage and load installed skills from standard locations.
+
+ OpenCode / Claude-style common locations (project-level first, then global):
+ - .agents/skills//SKILL.md
+ - .opencode/skills//SKILL.md
+ - .claude/skills//SKILL.md
+ - ~/.config/opencode/skills//SKILL.md
+ - ~/.claude/skills//SKILL.md
+
+ You may override with CODE_AGENT_SKILLS_DIR to point to a single root.
+ """
+
+ def __init__(self, repo_root: str, skills_root: Optional[str] = None):
+ super().__init__(
+ name="skills",
+ description=(
+ "Skills 工具:列出/搜索/加载已安装的技能(SOP/工作流)。"
+ "用于渐进式披露:先 list/search,再 show 具体 SKILL.md。"
+ ),
+ )
+ self.repo_root = Path(repo_root).expanduser().resolve()
+ env_skills_dir = env_stripped("CODE_AGENT_SKILLS_DIR", "")
+ override = (skills_root or "").strip() or (Path(env_skills_dir).as_posix() if env_skills_dir else "")
+ self.skills_roots: list[Path] = []
+ if override:
+ self.skills_roots = [Path(override).expanduser().resolve()]
+ else:
+ home = Path.home()
+ self.skills_roots = [
+ (self.repo_root / ".agents" / "skills").resolve(),
+ (self.repo_root / ".opencode" / "skills").resolve(),
+ (self.repo_root / ".claude" / "skills").resolve(),
+ (home / ".config" / "opencode" / "skills").resolve(),
+ (home / ".claude" / "skills").resolve(),
+ ]
+
+ def get_parameters(self) -> List[ToolParameter]:
+ return [
+ ToolParameter(
+ name="action",
+ type="string",
+ description="list | search | show",
+ required=True,
+ ),
+ ToolParameter(
+ name="id",
+ type="string",
+ description="技能 id(show 时必填),即 `.agents/skills//` 目录名",
+ required=False,
+ ),
+ ToolParameter(
+ name="query",
+ type="string",
+ description="搜索关键词(search 时必填)",
+ required=False,
+ ),
+ ToolParameter(
+ name="limit",
+ type="integer",
+ description="返回数量(list/search,默认 20)",
+ required=False,
+ default=20,
+ ),
+ ToolParameter(
+ name="roots",
+ type="boolean",
+ description="list 时是否同时输出扫描 roots(默认 false)",
+ required=False,
+ default=False,
+ ),
+ ]
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ action = (parameters.get("action") or "").strip().lower()
+ if action == "list":
+ limit = int(parameters.get("limit") or 20)
+ show_roots = bool(parameters.get("roots") or False)
+ return self._list(limit=limit, show_roots=show_roots)
+ if action == "search":
+ query = (parameters.get("query") or "").strip()
+ if not query:
+ return "错误:search 需要 query"
+ limit = int(parameters.get("limit") or 20)
+ return self._search(query=query, limit=limit)
+ if action == "show":
+ sid = (parameters.get("id") or "").strip()
+ if not sid:
+ return "错误:show 需要 id"
+ return self._show(skill_id=sid)
+ return "错误:action 必须是 list | search | show"
+
+ def _iter_skill_files(self) -> list[Path]:
+ seen: set[str] = set()
+ paths: list[Path] = []
+ for root in self.skills_roots:
+ if not root.exists() or not root.is_dir():
+ continue
+ try:
+ for d in sorted(root.iterdir(), key=lambda p: p.name.lower()):
+ if not d.is_dir():
+ continue
+ sid = d.name
+ if sid in seen:
+ continue
+ p = d / "SKILL.md"
+ if p.exists() and p.is_file():
+ seen.add(sid)
+ paths.append(p)
+ except Exception:
+ continue
+ return paths
+
+ def _load_meta(self, skill_file: Path) -> SkillMeta:
+ raw = skill_file.read_text(encoding="utf-8", errors="ignore")
+ meta, _body = _parse_front_matter(raw)
+ sid = skill_file.parent.name
+ name = meta.get("name") or sid
+ desc = meta.get("description") or ""
+ return SkillMeta(skill_id=sid, path=skill_file, name=name, description=desc)
+
+ def _list(self, limit: int = 20, show_roots: bool = False) -> str:
+ files = self._iter_skill_files()
+ if not files:
+ return "未找到 skills(检查目录:\n- " + "\n- ".join([p.as_posix() for p in self.skills_roots]) + "\n)"
+ metas = [self._load_meta(p) for p in files][: max(1, limit)]
+ lines = [f"找到 {len(metas)} 个 skills:"]
+ if show_roots:
+ lines.append("roots:")
+ for r in self.skills_roots:
+ lines.append(f"- {r.as_posix()}")
+ for m in metas:
+ lines.append(f"- {m.skill_id} ({m.name})")
+ if m.description:
+ lines.append(f" {m.description}")
+ return "\n".join(lines)
+
+ def _search(self, query: str, limit: int = 20) -> str:
+ q = query.lower()
+ files = self._iter_skill_files()
+ if not files:
+ return "未找到 skills(检查目录:\n- " + "\n- ".join([p.as_posix() for p in self.skills_roots]) + "\n)"
+ hits: list[SkillMeta] = []
+ for p in files:
+ m = self._load_meta(p)
+ if q in m.skill_id.lower() or q in m.name.lower() or q in (m.description or "").lower():
+ hits.append(m)
+ continue
+ # fallback: light content scan (first 2000 chars)
+ try:
+ raw = p.read_text(encoding="utf-8", errors="ignore")[:2000].lower()
+ if q in raw:
+ hits.append(m)
+ except Exception:
+ pass
+ if len(hits) >= limit:
+ break
+ if not hits:
+ return f"未找到匹配 '{query}' 的 skills。"
+ lines = [f"匹配 '{query}' 的 skills({len(hits)} 个):"]
+ for m in hits:
+ lines.append(f"- {m.skill_id} ({m.name})")
+ if m.description:
+ lines.append(f" {m.description}")
+ return "\n".join(lines)
+
+ def _show(self, skill_id: str) -> str:
+ # find in roots (project-first)
+ p: Optional[Path] = None
+ for root in self.skills_roots:
+ candidate = (root / skill_id / "SKILL.md").expanduser().resolve()
+ if candidate.exists():
+ p = candidate
+ break
+ if p is None:
+ return f"未找到 skill: {skill_id}"
+
+ raw = p.read_text(encoding="utf-8", errors="ignore")
+ meta, body = _parse_front_matter(raw)
+ header = f"[skill] {skill_id}"
+ if meta.get("name"):
+ header += f" ({meta['name']})"
+ out = [header]
+ if meta.get("description"):
+ out.append(meta["description"])
+ out.append("")
+ out.append(body.strip())
+ return "\n".join(out).strip()
+
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/terminal_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/terminal_tool.py
new file mode 100644
index 00000000..3b0094a4
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/terminal_tool.py
@@ -0,0 +1,784 @@
+"""TerminalTool - 命令行工具
+
+为Agent提供安全的命令行执行能力,支持:
+- 文件系统操作(ls, cat, head, tail, find, grep)
+- 文本处理(wc, sort, uniq)
+- 目录导航(pwd, cd)
+- 安全限制(白名单命令、路径限制、超时控制)
+
+使用场景:
+- JIT(即时)文件检索与分析
+- 代码仓库探索
+- 日志文件分析
+- 数据文件预览
+
+安全特性:
+- 命令白名单(只允许安全的只读命令)
+- 工作目录限制(沙箱)
+- 超时控制
+- 输出大小限制
+- 禁止危险操作(rm, mv, chmod等)
+"""
+
+from typing import Dict, Any, List, Optional
+import subprocess
+import os
+from pathlib import Path
+import shlex
+import re
+
+from ..base import Tool, ToolParameter
+
+
+class TerminalTool(Tool):
+ """命令行工具
+
+ 提供安全的命令行执行能力,支持常用的文件系统和文本处理命令。
+
+ 安全限制:
+ - 只允许白名单中的命令
+ - 限制在指定工作目录内
+ - 超时控制(默认30秒)
+ - 输出大小限制(默认10MB)
+
+ 用法示例:
+ ```python
+ terminal = TerminalTool(workspace="./project")
+
+ # 列出文件
+ result = terminal.run({"command": "ls -la"})
+
+ # 查看文件内容
+ result = terminal.run({"command": "cat README.md"})
+
+ # 搜索文件
+ result = terminal.run({"command": "grep -r 'TODO' src/"})
+
+ # 查看文件前10行
+ result = terminal.run({"command": "head -n 10 data.csv"})
+ ```
+ """
+
+ # 允许的命令白名单
+ # 这些命令被认为是安全的,主要用于文件查看、文本处理和信息获取
+ # 不包含可能修改系统或造成安全风险的命令(如rm、mv、chmod等)
+ ALLOWED_COMMANDS = {
+ # 文件列表与信息
+ 'ls', 'dir', 'tree',
+ # 文件内容查看
+ 'cat', 'head', 'tail', 'less', 'more',
+ # 文件搜索
+ 'find', 'grep', 'egrep', 'fgrep', 'rg',
+ # 文本处理
+ 'wc', 'sort', 'uniq', 'cut', 'awk', 'sed',
+ # shell 常见内建(用于管道/小脚本;仍受整体策略约束)
+ 'echo', 'printf',
+ # 目录/文件创建(受路径沙箱约束)
+ 'mkdir',
+ # 目录操作
+ 'pwd', 'cd',
+ # 文件信息
+ 'file', 'stat', 'du', 'df',
+ # 其他
+ 'which', 'whereis',
+ # 版本控制(只读子命令会被进一步限制)
+ 'git',
+ # 验证/测试(用于 /verify 验证闭环;仍受危险操作检测约束)
+ 'python', 'python3', 'pytest',
+ # Skills 生态:仅允许 `npx skills ...`(额外校验见 _is_allowed_npx)
+ 'npx',
+ }
+
+ # 常见 shell 元字符(用于检测"组合命令/写盘/子命令"等风险点;不再一刀切禁止)
+ # 这些元字符在shell中有特殊含义,可能用于组合命令或执行危险操作
+ # 系统会检测这些字符的存在,并根据安全策略决定是否允许执行
+ SHELL_META_TOKENS = ["|", "||", "&&", ";", ">", ">>", "<", "$(", "`"]
+
+ # 需要人类确认的高风险命令(MVP:只做识别;是否放行由上层策略决定)
+ # 这些命令可能对系统造成不可逆的修改,需要用户明确确认才能执行
+ DANGEROUS_BASE_COMMANDS = {"rm", "chmod"}
+ # Git的高风险子命令,可能造成代码丢失或历史修改
+ DANGEROUS_GIT_SUBCOMMANDS = {("reset", "--hard"), ("reset", "--hard", "HEAD")}
+
+ def __init__(
+ self,
+ workspace: str = ".",
+ timeout: int = 30,
+ max_output_size: int = 10 * 1024 * 1024, # 10MB
+ allow_cd: bool = True,
+ confirm_dangerous: bool = False,
+ default_shell_mode: bool = False,
+ ):
+ """初始化TerminalTool实例
+
+ Args:
+ workspace: 工作目录路径,所有命令将在此目录或其子目录中执行
+ timeout: 命令执行超时时间(秒),防止长时间运行的命令
+ max_output_size: 输出大小限制(字节),防止过大输出消耗资源
+ allow_cd: 是否允许cd命令,控制目录切换权限
+ confirm_dangerous: 是否在执行高风险命令时提示用户确认
+ default_shell_mode: 默认是否启用shell模式(支持管道、重定向等)
+ """
+ super().__init__(
+ name="terminal",
+ description="命令行工具 - 执行安全的文件系统、文本处理和代码执行命令(ls, cat, grep, head, tail等)"
+ )
+
+ # 将工作目录转换为绝对路径并规范化
+ self.workspace = Path(workspace).resolve()
+ self.timeout = timeout
+ self.max_output_size = max_output_size
+ self.allow_cd = allow_cd
+ self.confirm_dangerous = confirm_dangerous
+ self.default_shell_mode = default_shell_mode
+
+ # 当前工作目录(相对于workspace)
+ # 初始设置为工作目录根目录,可通过cd命令更改
+ self.current_dir = self.workspace
+
+ # 确保工作目录存在,如果不存在则创建
+ self.workspace.mkdir(parents=True, exist_ok=True)
+
+ def run(self, parameters: Dict[str, Any]) -> str:
+ """执行工具的主入口方法
+
+ 根据参数解析命令并执行,包含完整的安全检查流程:
+ 1. 参数验证 - 确保输入参数格式正确且必要参数存在
+ 2. 命令解析和分类 - 将命令字符串解析为可执行的参数列表
+ 3. 安全策略检查 - 多层安全验证,包括白名单检查、危险操作检测等
+ 4. 命令执行和结果处理 - 安全执行命令并格式化返回结果
+
+ 安全机制说明:
+ - 命令白名单:只执行预定义的安全命令,防止恶意命令执行
+ - 路径沙箱:所有文件操作限制在工作目录内,防止越权访问
+ - 危险操作确认:高风险命令需要用户明确确认才能执行
+ - 超时控制:防止长时间运行的命令消耗系统资源
+ - 输出限制:防止过大输出导致内存问题
+
+ Args:
+ parameters: 包含command、allow_dangerous、shell_mode等参数的字典
+ - command: 要执行的命令字符串
+ - allow_dangerous: 是否允许执行危险操作
+ - shell_mode: 是否启用shell模式(支持管道、重定向等)
+
+ Returns:
+ str: 命令执行结果或错误信息,包含详细的错误说明
+ """
+ # 第一步:参数验证 - 确保输入参数符合预期格式
+ if not self.validate_parameters(parameters):
+ return "❌ 参数验证失败"
+
+ # 提取并清理命令参数
+ command = parameters.get("command", "").strip()
+ allow_dangerous = bool(parameters.get("allow_dangerous", False))
+ shell_mode = bool(parameters.get("shell_mode", self.default_shell_mode))
+ # 由上层 UI 的二次确认注入:对齐 Claude Code/OpenCode/Codex 体验——用户点 y 后本次应当放行
+ user_approved = bool(parameters.get("user_approved", False))
+ effective_allow_dangerous = bool(allow_dangerous or user_approved)
+
+ # 基础安全检查:拒绝空命令,防止无意义的系统调用
+ if not command:
+ return "❌ 命令不能为空"
+
+ # 执行模式选择:
+ # - 如果命令不包含任何 shell 元字符(管道/重定向/命令替换等),即使命令来自默认 shell_mode,
+ # 也降级为 argv 模式执行:更安全,且避免 shell 静态白名单误判(例如 `npx skills ...`)。
+ # - 只有检测到 shell 元字符时才走真正的 shell_mode 执行分支。
+ if shell_mode:
+ has_meta = any(self._has_unquoted(command, tok) for tok in self.SHELL_META_TOKENS)
+ if has_meta:
+ return self._execute_shell(command, allow_dangerous=effective_allow_dangerous)
+ # no meta tokens -> treat as argv mode
+ shell_mode = False
+
+ # 第二步:命令解析 - 使用shlex进行安全的命令分割,处理引号和转义
+ try:
+ parts = shlex.split(command)
+ except ValueError as e:
+ return f"❌ 命令解析失败: {e}"
+
+ # 解析后再次验证,确保命令不为空
+ if not parts:
+ return "❌ 命令不能为空"
+
+ base_command = parts[0]
+
+ # 第三步:策略层 allow/ask/deny(默认 ask)
+ # - allow:白名单内的安全命令
+ # - deny:明确危险的基础命令(rm/chmod),除非用户已确认(user_approved)/或显式 allow_dangerous
+ # - ask:白名单外的命令,若 user_approved=True 则放行本次(一次性 token)
+ if base_command in self.DANGEROUS_BASE_COMMANDS and not effective_allow_dangerous:
+ return f"❌ 高风险命令 {base_command} 需要人类确认(allow_dangerous=true 或 user_approved=true)"
+
+ if base_command not in self.ALLOWED_COMMANDS and not user_approved:
+ return (
+ f"❌ 命令需要确认后放行(默认 ask):{base_command}\n"
+ "请在 UI 中确认后重试(UI 会注入 user_approved=true)。"
+ )
+
+ # 对 npx 做进一步限制:只允许 `npx skills ...`
+ if base_command == "npx" and not self._is_allowed_npx(parts):
+ return (
+ "❌ 仅允许执行 `npx skills ...`(用于外部 skills 查找/安装)。\n"
+ "示例:npx skills find react performance"
+ )
+
+ # 特殊命令处理:git命令需要额外的子命令安全检查
+ if base_command == "git":
+ # 如果用户已经在 UI 明确放行,则允许更广的 git 行为(仍受路径沙箱与超时限制)
+ if user_approved:
+ return self._execute_argv(parts, allow_dangerous=True)
+ return self._handle_git(parts, effective_allow_dangerous)
+
+ # 第四步:危险操作确认机制
+ # 当用户明确允许危险操作且启用了确认机制时,进行交互式确认
+ # 这为高风险操作提供了最后一道人工确认防线
+ if effective_allow_dangerous and self.confirm_dangerous and not user_approved:
+ ans = input(f"\n⚠️ 高风险命令:{command}\n允许执行?(y/n)\nconfirm> ").strip().lower()
+ if ans not in {"y", "yes"}:
+ return "⛔️ 已取消执行(用户未确认)。"
+
+ # 特殊命令处理:cd命令需要单独处理以维护工作目录状态
+ if base_command == 'cd':
+ return self._handle_cd(parts)
+
+ # 第五步:执行命令 - 通过所有安全检查后执行命令
+ return self._execute_argv(parts, allow_dangerous=effective_allow_dangerous)
+
+ def _is_allowed_npx(self, argv: List[str]) -> bool:
+ """仅允许 `npx skills ...`(允许带 npx 自身 flag,如 -y/--yes)。"""
+ if not argv or argv[0] != "npx":
+ return False
+ i = 1
+ # Skip npx flags (best-effort)
+ while i < len(argv) and argv[i].startswith("-"):
+ i += 1
+ if i >= len(argv):
+ return False
+ return argv[i] == "skills"
+
+ def get_parameters(self) -> List[ToolParameter]:
+ """获取工具参数定义"""
+ return [
+ ToolParameter(
+ name="command",
+ type="string",
+ description=(
+ f"要执行的命令(白名单: {', '.join(sorted(list(self.ALLOWED_COMMANDS)[:10]))}...)\n"
+ "示例: 'ls -la', 'cat file.txt', 'grep pattern *.py', 'head -n 20 data.csv'"
+ ),
+ required=True
+ ),
+ ToolParameter(
+ name="allow_dangerous",
+ type="boolean",
+ description="是否允许高风险命令(默认false;仅在用户明确确认后才可设置为true)",
+ required=False
+ ),
+ ToolParameter(
+ name="shell_mode",
+ type="boolean",
+ description="是否允许 shell 语义(管道/重定向/多段命令等)。默认继承工具配置。",
+ required=False,
+ ),
+ ToolParameter(
+ name="user_approved",
+ type="boolean",
+ description="是否已由上层 UI 二次确认放行(一次性 token)。通常由 UI 注入,模型不应自行设置。",
+ required=False,
+ ),
+ ]
+
+ def _contains_shell_meta(self, command: str) -> bool:
+ """检查命令中是否包含shell元字符
+
+ Args:
+ command: 要检查的命令字符串
+
+ Returns:
+ bool: 如果包含元字符返回True,否则返回False
+ """
+ return any(tok in command for tok in self.SHELL_META_TOKENS)
+
+ # --- shell parsing helpers (ignore operators inside quotes) ---
+ def _split_shell_segments(self, command: str) -> List[str]:
+ """
+ 按管道/逻辑与/逻辑或/分号操作符分割shell命令,忽略引号内的操作符。
+ 返回分割后的段列表(不包含操作符)。
+
+ 这个方法用于分析复杂的shell命令,确保每个段都是安全的。
+
+ Args:
+ command: 要分割的shell命令字符串
+
+ Returns:
+ List[str]: 分割后的命令段列表
+ """
+ ops = ["||", "&&", "|", ";"]
+ segs: List[str] = []
+ buf: List[str] = []
+ i = 0
+ quote: Optional[str] = None
+ while i < len(command):
+ ch = command[i]
+ if ch in {"'", '"'}:
+ if quote is None:
+ quote = ch
+ elif quote == ch:
+ quote = None
+ buf.append(ch)
+ i += 1
+ continue
+ if ch == "\\":
+ buf.append(ch)
+ if i + 1 < len(command):
+ buf.append(command[i + 1])
+ i += 2
+ else:
+ i += 1
+ continue
+ if quote is None:
+ matched = False
+ for op in ops:
+ if command.startswith(op, i):
+ seg = "".join(buf).strip()
+ if seg:
+ segs.append(seg)
+ buf = []
+ i += len(op)
+ matched = True
+ break
+ if matched:
+ continue
+ buf.append(ch)
+ i += 1
+ seg = "".join(buf).strip()
+ if seg:
+ segs.append(seg)
+ return segs
+
+ def _has_unquoted(self, command: str, token: str) -> bool:
+ """检查token(如>或$()或|)是否出现在引号外
+
+ 这个方法用于检测可能存在安全风险的shell操作符,
+ 确保它们不是在引号内(引号内是安全的字符串字面量)。
+
+ Args:
+ command: 要检查的命令字符串
+ token: 要查找的token字符串
+
+ Returns:
+ bool: 如果token出现在引号外返回True,否则返回False
+ """
+ q: Optional[str] = None
+ i = 0
+ while i < len(command):
+ ch = command[i]
+ if ch in {"'", '"'}:
+ if q is None:
+ q = ch
+ elif q == ch:
+ q = None
+ i += 1
+ continue
+ if ch == "\\":
+ i += 2
+ continue
+ if q is None and command.startswith(token, i):
+ return True
+ i += 1
+ return False
+
+ def _shell_requires_allow_dangerous(self, command: str) -> bool:
+ """检查shell命令是否需要危险操作权限
+
+ 此方法分析shell命令,判断其是否包含可能对系统造成风险的操作。
+ 这是安全机制的重要组成部分,用于识别需要额外权限确认的命令。
+
+ 安全检查逻辑:
+ 1. 检查文件写入操作(>、>>重定向),但排除到/dev/null的重定向
+ 2. 检查命令替换(`command`或$(command))
+ 3. 检查已知的危险基础命令(rm、chmod等)
+ 4. 检查Git的危险子命令(如reset --hard)
+
+ Args:
+ command: 要检查的shell命令字符串
+
+ Returns:
+ bool: 如果命令需要危险操作权限则返回True,否则返回False
+
+ Note:
+ - 此方法不会阻止命令执行,只是标识需要额外确认的命令
+ - 实际的权限检查在执行阶段进行
+ - 采用相对保守的策略,对/dev/null重定向等安全操作给予宽容
+ """
+ # 写盘/命令替换通常视为高风险,但常见的只读掩埋(如 2>/dev/null、|| echo)可放宽
+ # 宽容规则:仅当重定向目标不是 /dev/null 时才视为写盘;简单的 "|| echo ..." 视为安全。
+ if self._has_unquoted(command, ">") or self._has_unquoted(command, ">>"):
+ # 忽略 /dev/null 重定向(这是安全的丢弃输出操作)
+ if re.search(r">\s*/dev/null", command) or re.search(r">>\s*/dev/null", command):
+ pass
+ else:
+ # 其他重定向操作可能修改文件内容,需要危险权限
+ return True
+
+ # 检查命令替换:`command`(反引号)和 $(command) 格式
+ # 命令替换可能执行任意代码,存在代码注入风险
+ if self._has_unquoted(command, "$(") or self._has_unquoted(command, "`"):
+ return True
+
+ # 检查已知的危险基础命令
+ # 这些命令可能对系统造成不可逆的影响,需要特别关注
+ if re.search(r"(^|\s)rm(\s|$)", command):
+ return True
+ if re.search(r"(^|\s)chmod(\s|$)", command):
+ return True
+
+ # 检查Git的危险子命令
+ # git reset --hard 可能导致代码丢失,属于高风险操作
+ if re.search(r"git\s+reset\s+--hard", command):
+ return True
+
+ # 如果没有检测到危险操作,则不需要额外权限
+ return False
+
+ def _shell_all_commands_whitelisted(self, command: str) -> bool:
+ """
+ 静态检查shell命令中的所有段是否都在白名单中(尽力而为的检查)
+
+ 此方法通过分割shell命令为多个段,然后检查每个段的第一个命令是否在白名单中。
+ 这是对shell命令的安全预检查,用于在未允许危险操作时确保命令的安全性。
+
+ 安全检查策略:
+ 1. 使用shell元字符分割命令为多个独立段
+ 2. 对每个段进行命令解析,提取基础命令
+ 3. 检查基础命令是否在白名单中
+ 4. 对git命令进行特殊处理,只允许安全的只读子命令
+
+ Args:
+ command: 要检查的完整shell命令字符串
+
+ Returns:
+ bool: 如果所有命令段都在白名单中则返回True,否则返回False
+
+ Note:
+ - 这是一个尽力而为的检查,不能保证100%准确
+ - 对于复杂的shell语法,可能存在误判
+ - git命令只允许status和diff子命令,其他子命令被视为危险操作
+ """
+ # 预处理:将换行符替换为空格,便于统一处理
+ cmd = command.replace("\n", " ")
+
+ # 分割命令为多个段,每个段包含一个独立的命令
+ segments = self._split_shell_segments(cmd)
+
+ # 对每个命令段进行安全检查
+ for seg in segments:
+ seg = seg.strip()
+ if not seg:
+ continue # 跳过空段
+
+ try:
+ # 使用shlex进行安全的命令分割,处理引号和转义
+ argv = shlex.split(seg)
+ except Exception:
+ # 如果解析失败,认为不安全
+ return False
+
+ if not argv:
+ continue # 跳过空命令段
+
+ # 获取基础命令(命令名的第一部分)
+ base = argv[0]
+
+ # 检查基础命令是否在白名单中
+ if base not in self.ALLOWED_COMMANDS:
+ return False
+
+ # npx 仅允许 `npx skills ...`
+ if base == "npx" and not self._is_allowed_npx(argv):
+ return False
+
+ # 对git命令进行特殊处理
+ # 只允许安全的只读子命令,其他子命令被视为危险操作
+ if base == "git":
+ if len(argv) < 2:
+ return False # git命令必须包含子命令
+ if argv[1] not in {"status", "diff"}:
+ return False # 只允许status和diff子命令
+
+ # 所有命令段都通过了安全检查
+ return True
+
+ def _execute_shell(self, command: str, allow_dangerous: bool = False) -> str:
+ """
+ 执行shell命令字符串(支持Claude Code风格的shell特性)
+
+ 此方法提供安全的shell命令执行能力,支持管道、重定向、命令替换等复杂shell语法。
+ 通过多层安全检查机制确保命令执行的安全性。
+
+ 安全防护措施:
+ - 如果shell命令包含重定向/命令替换/已知危险操作 -> 需要allow_dangerous权限
+ - 如果未允许危险操作 -> 要求所有命令段都在白名单中(尽力而为的检查)
+ - confirm_dangerous可以提示用户确认包含shell元字符或需要allow_dangerous的命令
+
+ 执行流程:
+ 1. 检查命令是否需要危险权限
+ 2. 验证命令白名单(如果未允许危险操作)
+ 3. 用户确认(如果启用confirm_dangerous)
+ 4. 执行命令并处理输出
+ 5. 返回执行结果或错误信息
+
+ Args:
+ command: 要执行的完整shell命令字符串
+ allow_dangerous: 是否允许执行危险操作(默认False)
+
+ Returns:
+ str: 命令执行结果或错误信息
+
+ Note:
+ - 支持管道操作而无需确认(如 'ls | grep .py')
+ - 只有在可能写入文件/转义/执行危险操作时才需要确认
+ - 输出会被截断以防止内存问题
+ """
+ needs_allow = self._shell_requires_allow_dangerous(command)
+
+ # 第一层安全检查:危险操作检测
+ # 如果命令包含危险操作但未允许危险操作,则拒绝执行
+ # 这是第一道安全防线,防止潜在的恶意操作
+ if needs_allow and not allow_dangerous:
+ return "❌ 该命令包含写盘/子命令替换/高风险操作,需用户确认后再执行(allow_dangerous=true)"
+
+ # 第二层安全检查:白名单验证
+ # 如果未允许危险操作,则检查所有命令段是否都在白名单中
+ # 这是对命令的进一步安全验证,确保只能执行预定义的安全命令
+ if not allow_dangerous and not self._shell_all_commands_whitelisted(command):
+ return "❌ shell_mode 下检测到非白名单命令/不允许的 git 子命令。需要用户确认后再执行(allow_dangerous=true)"
+
+ # 在 TUI/Agent 已经完成二次确认的情况下(allow_dangerous=true),
+ # 不再通过 stdin 进行交互式确认,避免后台线程卡死。
+ if self.confirm_dangerous and needs_allow and not allow_dangerous:
+ ans = input(f"\n⚠️ 即将执行高风险 shell 命令:{command}\n允许执行?(y/n)\nconfirm> ").strip().lower()
+ if ans not in {"y", "yes"}:
+ return "⛔️ 已取消执行(用户未确认)。"
+
+ try:
+ result = subprocess.run(
+ command,
+ shell=True,
+ cwd=str(self.current_dir),
+ capture_output=True,
+ text=True,
+ timeout=self.timeout,
+ env=os.environ.copy(),
+ )
+
+ output = (result.stdout or "") + (result.stderr or "")
+ if len(output.encode("utf-8", errors="ignore")) > self.max_output_size:
+ output = output[: self.max_output_size] + "\n...output truncated...\n"
+
+ if result.returncode != 0:
+ return f"命令执行失败 (返回码 {result.returncode}):\n{output}"
+ return output.strip() if output.strip() else "(no output)"
+ except subprocess.TimeoutExpired:
+ return f"❌ 命令超时(>{self.timeout}s)"
+ except Exception as e:
+ return f"❌ 命令执行异常: {e}"
+
+ def _handle_cd(self, parts: List[str]) -> str:
+ """处理cd命令,实现安全的目录切换
+
+ 此方法专门处理cd命令,确保目录切换在允许的工作空间范围内进行。
+ 支持绝对路径、相对路径和特殊路径(如..、~)的处理。
+
+ Args:
+ parts: cd命令的参数列表,parts[1]是目标路径
+
+ Returns:
+ str: 执行结果或错误信息
+
+ Note:
+ - 只允许切换到工作空间内的目录
+ - 支持路径规范化,处理..和.等相对路径
+ - 不允许切换到工作空间之外的目录
+ """
+ if not self.allow_cd:
+ return "❌ cd 命令已禁用"
+
+ if len(parts) < 2:
+ # cd命令没有参数时,返回当前目录信息
+ return f"当前目录: {self.current_dir}"
+
+ target_dir = parts[1]
+
+ # 处理特殊的相对路径
+ if target_dir == "..":
+ # 切换到父目录
+ new_dir = self.current_dir.parent
+ elif target_dir == ".":
+ # 当前目录
+ new_dir = self.current_dir
+ elif target_dir == "~":
+ # 切换到工作目录根目录
+ new_dir = self.workspace
+ else:
+ # 解析相对或绝对路径
+ new_dir = (self.current_dir / target_dir).resolve()
+
+ # 检查新目录是否在工作空间范围内
+ try:
+ new_dir.relative_to(self.workspace)
+ except ValueError:
+ return f"❌ 不允许访问工作目录外的路径: {new_dir}"
+
+ # 验证目录存在性
+ if not new_dir.exists():
+ return f"❌ 目录不存在: {new_dir}"
+
+ # 确保目标是目录而非文件
+ if not new_dir.is_dir():
+ return f"❌ 不是目录: {new_dir}"
+
+ # 更新当前工作目录
+ self.current_dir = new_dir
+ return f"✅ 切换到目录: {self.current_dir}"
+
+ def _execute_argv(self, argv: List[str], allow_dangerous: bool = False) -> str:
+ """执行参数向量形式的命令(不使用shell解释)
+
+ 此方法直接执行命令及其参数,不通过shell解释,因此不支持管道、重定向等shell特性。
+ 这种方式更安全,因为避免了shell注入攻击的风险,但功能相对有限。
+
+ 安全检查流程:
+ 1. 检查高风险命令(rm、chmod等)是否需要用户确认
+ 2. 对允许的高风险命令进行路径沙箱限制
+ 3. 对mkdir命令进行路径沙箱限制
+ 4. 执行命令并处理结果
+
+ Args:
+ argv: 命令及其参数的列表,argv[0]是命令名,其余是参数
+ allow_dangerous: 是否允许执行高风险命令(默认False)
+
+ Returns:
+ str: 命令执行结果或错误信息
+
+ Note:
+ - 不支持shell特性如管道、重定向、变量替换等
+ - 更安全,避免了shell注入攻击
+ - 适用于简单的命令执行场景
+ - 所有路径操作都被限制在工作空间内
+ """
+ # 第一层安全检查:高风险命令二次门禁
+ # 对明确高风险基命令(rm/chmod等)进行额外检查
+ # 这些命令可能对系统造成不可逆的修改,需要用户明确确认
+ # 这是安全防护的第一道防线,防止意外的系统修改
+ if argv and argv[0] in self.DANGEROUS_BASE_COMMANDS and not allow_dangerous:
+ return f"❌ 高风险命令 {argv[0]} 需要人类确认(allow_dangerous=true)"
+
+ # 第二层安全检查:路径沙箱限制
+ # 对带路径参数的高风险命令做路径沙箱限制(仅当放行时)
+ # 确保即使允许执行危险命令,也只能在工作空间内操作
+ # 这防止了恶意用户通过相对路径或符号链接逃逸沙箱
+ if argv and argv[0] in {"rm", "chmod"} and allow_dangerous:
+ # 保守检查:所有看起来像路径的非标志参数都必须在工作空间内
+ # 使用current_dir作为基准,确保相对路径也被正确限制
+ for a in argv[1:]:
+ if a.startswith("-"):
+ continue # 跳过选项参数(如 -r, -f 等)
+ candidate = (self.current_dir / a).resolve()
+ try:
+ # 检查解析后的绝对路径是否在工作空间内
+ candidate.relative_to(self.workspace)
+ except ValueError:
+ # 路径超出工作空间范围,拒绝执行
+ return f"❌ 拒绝在工作目录外操作: {a}"
+
+ # 第三层安全检查:mkdir命令路径沙箱
+ # mkdir虽然相对安全,但仍需确保不会在工作空间外创建目录
+ # 这防止了通过mkdir命令在工作空间外创建后门或敏感目录
+ if argv and argv[0] == "mkdir":
+ for a in argv[1:]:
+ if a.startswith("-"):
+ continue # 跳过选项参数(如 -p, -m 等)
+ candidate = (self.current_dir / a).resolve()
+ try:
+ # 检查要创建的目录是否在工作空间内
+ candidate.relative_to(self.workspace)
+ except ValueError:
+ # 尝试在工作空间外创建目录,拒绝执行
+ return f"❌ 不允许在工作目录外创建目录: {a}"
+
+ # npx 常会提示“是否安装依赖(y/n)”而阻塞在 stdin。
+ # 我们的工具执行是非交互式的(capture_output),因此默认给 npx 注入 `-y` 以避免卡死。
+ if argv and argv[0] == "npx":
+ has_yes = any(a in {"-y", "--yes"} or a.startswith("--yes=") for a in argv[1:3])
+ if not has_yes:
+ argv = ["npx", "-y"] + argv[1:]
+
+ try:
+ # 直接执行命令,不通过shell
+ # 使用shell=False确保命令不被shell解释,避免注入攻击
+ # 使用capture_output=True捕获标准输出和标准错误
+ # 使用text=True确保输出为文本格式
+ result = subprocess.run(
+ argv,
+ shell=False,
+ cwd=str(self.current_dir),
+ capture_output=True,
+ text=True,
+ timeout=self.timeout,
+ env=os.environ.copy(),
+ )
+
+ # 合并标准输出和标准错误
+ # 这样用户可以看到完整的执行结果
+ output = result.stdout
+ if result.stderr:
+ output += f"\n[stderr]\n{result.stderr}"
+
+ # 检查输出大小,防止过大输出消耗内存
+ if len(output) > self.max_output_size:
+ output = output[:self.max_output_size]
+ output += f"\n\n⚠️ 输出被截断(超过 {self.max_output_size} 字节)"
+
+ # 添加返回码信息,帮助用户了解命令执行状态
+ if result.returncode != 0:
+ output = f"⚠️ 命令返回码: {result.returncode}\n\n{output}"
+
+ return output if output else "✅ 命令执行成功(无输出)"
+
+ except subprocess.TimeoutExpired:
+ # 命令执行超时,可能是死循环或处理大量数据
+ return f"❌ 命令执行超时(超过 {self.timeout} 秒)"
+ except Exception as e:
+ # 捕获其他异常,如权限错误、文件不存在等
+ return f"❌ 命令执行失败: {e}"
+
+ def _truncate_output(self, output: str) -> str:
+ """截断过大的输出,防止内存问题
+
+ 此方法检查输出字符串的长度,如果超过配置的最大输出大小,
+ 则截断输出并添加提示信息,防止过大的输出消耗过多内存。
+
+ Args:
+ output: 需要检查的输出字符串
+
+ Returns:
+ str: 原始输出或截断后的输出(如果超过大小限制)
+
+ Note:
+ - 截断时会保留开头的内容,丢弃超出限制的部分
+ - 会在截断的输出末尾添加提示信息,说明输出已被截断
+ """
+ if len(output) > self.max_output_size:
+ return output[: self.max_output_size] + f"\n[输出被截断,超过 {self.max_output_size} 字节限制]"
+ return output
+
+ def get_current_dir(self) -> str:
+ """获取当前工作目录"""
+ return str(self.current_dir)
+
+ def reset_dir(self):
+ """重置到工作目录根"""
+ self.current_dir = self.workspace
+
diff --git a/Co-creation-projects/aug618-Praxis/tools/builtin/todo_tool.py b/Co-creation-projects/aug618-Praxis/tools/builtin/todo_tool.py
new file mode 100644
index 00000000..887e2102
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/builtin/todo_tool.py
@@ -0,0 +1,213 @@
+"""TodoTool - 轻量级待办板
+
+MVP 目标:
+- 仅支持 add / list / update
+- 状态枚举:pending | in_progress | completed(强约束:同时最多 1 个 in_progress)
+- 存储:.helloagents/todos/todos.json,原子写入 + 简单备份
+- 输出:按状态分组的要点列表,便于 LLM 消化
+"""
+
+from __future__ import annotations
+
+import json
+from dataclasses import dataclass, asdict
+from datetime import datetime
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from ..base import Tool, ToolParameter
+
+
+STATUSES = ("pending", "in_progress", "completed")
+
+
+@dataclass
+class TodoItem:
+ id: int
+ title: str
+ desc: str = ""
+ status: str = "pending"
+ created_at: str = ""
+ updated_at: str = ""
+
+ @classmethod
+ def from_dict(cls, data: Dict[str, Any]) -> "TodoItem":
+ return cls(
+ id=int(data["id"]),
+ title=data.get("title", ""),
+ desc=data.get("desc", ""),
+ status=data.get("status", "pending"),
+ created_at=data.get("created_at", ""),
+ updated_at=data.get("updated_at", ""),
+ )
+
+
+class TodoTool(Tool):
+ def __init__(self, workspace: str):
+ super().__init__(
+ name="todo",
+ description="待办工具:add/list/update;状态 pending|in_progress|completed(同时仅允许1个in_progress)",
+ )
+ self.workspace = Path(workspace)
+ self.workspace.mkdir(parents=True, exist_ok=True)
+ self.data_file = self.workspace / "todos.json"
+ self.backup_file = self.workspace / "todos.json.bak"
+ if not self.data_file.exists():
+ self._save({"items": []})
+
+ def get_parameters(self) -> List[ToolParameter]:
+ return [
+ ToolParameter(name="action", type="string", description="add | list | update", required=True),
+ ToolParameter(name="title", type="string", description="待办标题(add必填,update可选)", required=False),
+ ToolParameter(name="desc", type="string", description="待办描述(可选)", required=False),
+ ToolParameter(name="status", type="string", description="pending|in_progress|completed(update可选)", required=False),
+ ToolParameter(name="id", type="integer", description="要更新的待办ID(update必填)", required=False),
+ ]
+
+ # ---------------- core ops ----------------
+ def run(self, parameters: Dict[str, Any]) -> str:
+ if not self.validate_parameters(parameters):
+ return "参数缺失,需包含 action(add/list/update)。"
+ action = str(parameters.get("action", "")).strip().lower().rstrip("]")
+ if action == "add":
+ return self._add(title=parameters.get("title", ""), desc=parameters.get("desc", ""), status=parameters.get("status", "pending"))
+ if action == "list":
+ return self._list(status_filter=parameters.get("status"))
+ if action == "update":
+ return self._update(
+ todo_id=parameters.get("id"),
+ title=parameters.get("title"),
+ desc=parameters.get("desc"),
+ status=parameters.get("status"),
+ )
+ return "不支持的 action,应为 add/list/update。"
+
+ # ---------------- storage ----------------
+ def _load(self) -> Dict[str, Any]:
+ with open(self.data_file, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+ def _save(self, data: Dict[str, Any]) -> None:
+ tmp = self.data_file.with_suffix(".tmp")
+ if self.data_file.exists():
+ try:
+ self.backup_file.write_text(self.data_file.read_text(encoding="utf-8"), encoding="utf-8")
+ except Exception:
+ pass
+ tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
+ tmp.replace(self.data_file)
+
+ # ---------------- helpers ----------------
+ def _next_id(self, items: List[TodoItem]) -> int:
+ return (max([i.id for i in items], default=0) + 1) if items else 1
+
+ def _now(self) -> str:
+ return datetime.now().isoformat(timespec="seconds")
+
+ def _enforce_single_in_progress(self, items: List[TodoItem], incoming_status: str, incoming_id: Optional[int]) -> Optional[str]:
+ if incoming_status != "in_progress":
+ return None
+ for it in items:
+ if it.status == "in_progress" and (incoming_id is None or it.id != incoming_id):
+ return f"已有进行中的任务 #{it.id}《{it.title}》。先完成/更新它后再切换。"
+ return None
+
+ # ---------------- actions ----------------
+ def _add(self, title: str, desc: str, status: str) -> str:
+ title = (title or "").strip()
+ if not title:
+ return "❌ add 失败:title 不能为空。"
+ status = status if status in STATUSES else "pending"
+ data = self._load()
+ items = [TodoItem.from_dict(i) for i in data.get("items", [])]
+ conflict = self._enforce_single_in_progress(items, status, None)
+ if conflict:
+ return f"❌ add 失败:{conflict}"
+ now = self._now()
+ new_item = TodoItem(id=self._next_id(items), title=title, desc=desc or "", status=status, created_at=now, updated_at=now)
+ items.append(new_item)
+ self._save({"items": [asdict(i) for i in items]})
+ return f"✅ 已添加 #{new_item.id} [{new_item.status}] {new_item.title}"
+
+ def _update(self, todo_id: Any, title: Optional[str], desc: Optional[str], status: Optional[str]) -> str:
+ try:
+ tid = int(todo_id)
+ except Exception:
+ return "❌ update 失败:缺少有效的 id。"
+ if status and status not in STATUSES:
+ return "❌ update 失败:status 必须是 pending|in_progress|completed。"
+
+ data = self._load()
+ items = [TodoItem.from_dict(i) for i in data.get("items", [])]
+ target = next((i for i in items if i.id == tid), None)
+ if not target:
+ return f"❌ update 失败:未找到 id={tid} 的任务。"
+
+ conflict = self._enforce_single_in_progress(items, status or target.status, tid)
+ if conflict:
+ return f"❌ update 失败:{conflict}"
+
+ changed = []
+ if title is not None:
+ target.title = title.strip()
+ changed.append("title")
+ if desc is not None:
+ target.desc = desc
+ changed.append("desc")
+ if status is not None:
+ target.status = status
+ changed.append("status")
+ target.updated_at = self._now()
+
+ self._save({"items": [asdict(i) for i in items]})
+ if not changed:
+ return f"⚠️ 未修改任何字段 #{tid}"
+ return f"✅ 已更新 #{tid} ({', '.join(changed)}) -> [{target.status}] {target.title}"
+
+ def _list(self, status_filter: Optional[str]) -> str:
+ data = self._load()
+ items = [TodoItem.from_dict(i) for i in data.get("items", [])]
+ if status_filter and status_filter in STATUSES:
+ items = [i for i in items if i.status == status_filter]
+
+ groups = {"in_progress": [], "pending": [], "completed": []}
+ for it in items:
+ groups.setdefault(it.status, []).append(it)
+
+ # ANSI colors similar to v2_todo_agent
+ COLOR_PENDING = "\x1b[38;2;176;176;176m"
+ COLOR_PROGRESS = "\x1b[38;2;120;200;255m"
+ COLOR_DONE = "\x1b[38;2;34;139;34m"
+ RESET = "\x1b[0m"
+
+ def fmt(group_name: str, arr: List[TodoItem]) -> str:
+ if not arr:
+ return ""
+ lines = [f"[{group_name.upper()}]"]
+ for it in sorted(arr, key=lambda x: x.id):
+ mark = "☒" if it.status == "completed" else "☐"
+ color = COLOR_PENDING
+ if it.status == "in_progress":
+ color = COLOR_PROGRESS
+ elif it.status == "completed":
+ color = COLOR_DONE
+ line_main = f"- {mark} #{it.id} {it.title} (updated {it.updated_at})"
+ line_desc = f" {it.desc}" if it.desc else None
+
+ if it.status == "completed":
+ line_main = f"{color}\x1b[9m{line_main}{RESET}"
+ if line_desc:
+ line_desc = f"{color}\x1b[9m{line_desc}{RESET}"
+ else:
+ line_main = f"{color}{line_main}{RESET}"
+ if line_desc:
+ line_desc = f"{color}{line_desc}{RESET}"
+
+ lines.append(line_main)
+ if line_desc:
+ lines.append(line_desc)
+ return "\n".join(lines)
+
+ parts = [fmt("in_progress", groups["in_progress"]), fmt("pending", groups["pending"]), fmt("completed", groups["completed"])]
+ out = "\n\n".join([p for p in parts if p])
+ return out or "暂无待办。"
diff --git a/Co-creation-projects/aug618-Praxis/tools/chain.py b/Co-creation-projects/aug618-Praxis/tools/chain.py
new file mode 100644
index 00000000..eeb66c7e
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/chain.py
@@ -0,0 +1,166 @@
+"""工具链管理器 - HelloAgents工具链式调用支持"""
+
+from typing import List, Dict, Any, Optional
+from .registry import ToolRegistry
+
+
+class ToolChain:
+ """工具链 - 支持多个工具的顺序执行"""
+
+ def __init__(self, name: str, description: str):
+ self.name = name
+ self.description = description
+ self.steps: List[Dict[str, Any]] = []
+
+ def add_step(self, tool_name: str, input_template: str, output_key: str = None):
+ """
+ 添加工具执行步骤
+
+ Args:
+ tool_name: 工具名称
+ input_template: 输入模板,支持变量替换,如 "{input}" 或 "{search_result}"
+ output_key: 输出结果的键名,用于后续步骤引用
+ """
+ step = {
+ "tool_name": tool_name,
+ "input_template": input_template,
+ "output_key": output_key or f"step_{len(self.steps)}_result"
+ }
+ self.steps.append(step)
+ print(f"✅ 工具链 '{self.name}' 添加步骤: {tool_name}")
+
+ def execute(self, registry: ToolRegistry, input_data: str, context: Dict[str, Any] = None) -> str:
+ """
+ 执行工具链
+
+ Args:
+ registry: 工具注册表
+ input_data: 初始输入数据
+ context: 执行上下文,用于变量替换
+
+ Returns:
+ 最终执行结果
+ """
+ if not self.steps:
+ return "❌ 工具链为空,无法执行"
+
+ print(f"🚀 开始执行工具链: {self.name}")
+
+ # 初始化上下文
+ if context is None:
+ context = {}
+ context["input"] = input_data
+
+ final_result = input_data
+
+ for i, step in enumerate(self.steps):
+ tool_name = step["tool_name"]
+ input_template = step["input_template"]
+ output_key = step["output_key"]
+
+ print(f"📝 执行步骤 {i+1}/{len(self.steps)}: {tool_name}")
+
+ # 替换模板中的变量
+ try:
+ actual_input = input_template.format(**context)
+ except KeyError as e:
+ return f"❌ 模板变量替换失败: {e}"
+
+ # 执行工具
+ try:
+ result = registry.execute_tool(tool_name, actual_input)
+ context[output_key] = result
+ final_result = result
+ print(f"✅ 步骤 {i+1} 完成")
+ except Exception as e:
+ return f"❌ 工具 '{tool_name}' 执行失败: {e}"
+
+ print(f"🎉 工具链 '{self.name}' 执行完成")
+ return final_result
+
+
+class ToolChainManager:
+ """工具链管理器"""
+
+ def __init__(self, registry: ToolRegistry):
+ self.registry = registry
+ self.chains: Dict[str, ToolChain] = {}
+
+ def register_chain(self, chain: ToolChain):
+ """注册工具链"""
+ self.chains[chain.name] = chain
+ print(f"✅ 工具链 '{chain.name}' 已注册")
+
+ def execute_chain(self, chain_name: str, input_data: str, context: Dict[str, Any] = None) -> str:
+ """执行指定的工具链"""
+ if chain_name not in self.chains:
+ return f"❌ 工具链 '{chain_name}' 不存在"
+
+ chain = self.chains[chain_name]
+ return chain.execute(self.registry, input_data, context)
+
+ def list_chains(self) -> List[str]:
+ """列出所有已注册的工具链"""
+ return list(self.chains.keys())
+
+ def get_chain_info(self, chain_name: str) -> Optional[Dict[str, Any]]:
+ """获取工具链信息"""
+ if chain_name not in self.chains:
+ return None
+
+ chain = self.chains[chain_name]
+ return {
+ "name": chain.name,
+ "description": chain.description,
+ "steps": len(chain.steps),
+ "step_details": [
+ {
+ "tool_name": step["tool_name"],
+ "input_template": step["input_template"],
+ "output_key": step["output_key"]
+ }
+ for step in chain.steps
+ ]
+ }
+
+
+# 便捷函数
+def create_research_chain() -> ToolChain:
+ """创建一个研究工具链:搜索 -> 计算 -> 总结"""
+ chain = ToolChain(
+ name="research_and_calculate",
+ description="搜索信息并进行相关计算"
+ )
+
+ # 步骤1:搜索信息
+ chain.add_step(
+ tool_name="search",
+ input_template="{input}",
+ output_key="search_result"
+ )
+
+ # 步骤2:基于搜索结果进行计算
+ chain.add_step(
+ tool_name="my_calculator",
+ input_template="2 + 2", # 简单的计算示例
+ output_key="calc_result"
+ )
+
+ return chain
+
+
+def create_simple_chain() -> ToolChain:
+ """创建一个简单的工具链示例"""
+ chain = ToolChain(
+ name="simple_demo",
+ description="简单的工具链演示"
+ )
+
+ # 只包含一个计算步骤
+ chain.add_step(
+ tool_name="my_calculator",
+ input_template="{input}",
+ output_key="result"
+ )
+
+ return chain
diff --git a/Co-creation-projects/aug618-Praxis/tools/registry.py b/Co-creation-projects/aug618-Praxis/tools/registry.py
new file mode 100644
index 00000000..544ac849
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/tools/registry.py
@@ -0,0 +1,361 @@
+"""工具注册表 - HelloAgents原生工具系统"""
+
+from typing import Optional, Any, Callable
+import json
+import time
+import uuid
+from .base import Tool
+from utils.observability import log_event
+
+class ToolRegistry:
+ """
+ HelloAgents工具注册表
+
+ 提供工具的注册、管理和执行功能。
+ 支持两种工具注册方式:
+ 1. Tool对象注册(推荐)
+ 2. 函数直接注册(简便)
+ """
+
+ def __init__(self):
+ self._tools: dict[str, Tool] = {}
+ self._functions: dict[str, dict[str, Any]] = {}
+
+ def register_tool(self, tool: Tool):
+ """
+ 注册Tool对象
+
+ Args:
+ tool: Tool实例
+ """
+ if tool.name in self._tools:
+ print(f"⚠️ 警告:工具 '{tool.name}' 已存在,将被覆盖。")
+
+ self._tools[tool.name] = tool
+ #print(f"✅ 工具 '{tool.name}' 已注册。")
+
+ def register_function(self, name: str, description: str, func: Callable[[str], str]):
+ """
+ 直接注册函数作为工具(简便方式)
+
+ Args:
+ name: 工具名称
+ description: 工具描述
+ func: 工具函数,接受字符串参数,返回字符串结果
+ """
+ if name in self._functions:
+ print(f"⚠️ 警告:工具 '{name}' 已存在,将被覆盖。")
+
+ self._functions[name] = {
+ "description": description,
+ "func": func
+ }
+ #print(f"✅ 工具 '{name}' 已注册。")
+
+ def unregister(self, name: str):
+ """注销工具"""
+ if name in self._tools:
+ del self._tools[name]
+ print(f"🗑️ 工具 '{name}' 已注销。")
+ elif name in self._functions:
+ del self._functions[name]
+ print(f"🗑️ 工具 '{name}' 已注销。")
+ else:
+ print(f"⚠️ 工具 '{name}' 不存在。")
+
+ def get_tool(self, name: str) -> Optional[Tool]:
+ """获取Tool对象"""
+ return self._tools.get(name)
+
+ def get_function(self, name: str) -> Optional[Callable]:
+ """获取工具函数"""
+ func_info = self._functions.get(name)
+ return func_info["func"] if func_info else None
+
+ def execute_tool(self, name: str, input_text: str) -> str:
+ """
+ 执行工具
+
+ Args:
+ name: 工具名称
+ input_text: 输入参数
+
+ Returns:
+ 工具执行结果
+ """
+ # 优先查找Tool对象
+ if name in self._tools:
+ tool = self._tools[name]
+ try:
+ start = time.time()
+ raw = (input_text or "").strip()
+ tool_call_id = uuid.uuid4().hex[:12]
+
+ # 预处理:如果输入包含换行和另一个 Action,只取第一行
+ if '\n' in raw and 'Action:' in raw:
+ lines = raw.split('\n')
+ raw = lines[0].strip()
+
+ def _scrub(obj: Any) -> Any:
+ """脱敏:避免把 key/token/password 等写进日志。"""
+ try:
+ if isinstance(obj, dict):
+ out = {}
+ for k, v in obj.items():
+ lk = str(k).lower()
+ if any(s in lk for s in ["api_key", "apikey", "token", "password", "secret", "key"]):
+ out[k] = "***"
+ else:
+ out[k] = _scrub(v)
+ return out
+ if isinstance(obj, list):
+ return [_scrub(x) for x in obj[:50]]
+ if isinstance(obj, str):
+ return obj if len(obj) <= 2000 else (obj[:2000] + "...")
+ return obj
+ except Exception:
+ return "***"
+
+ def _preview(text: Any, limit: int = 1200) -> str:
+ try:
+ s = text if isinstance(text, str) else json.dumps(text, ensure_ascii=False)
+ except Exception:
+ s = str(text)
+ s = s.strip()
+ if len(s) <= limit:
+ return s
+ return s[:limit] + "..."
+
+ # 1) JSON 直通:允许 ReAct 里用 tool[{"k":"v"}] 精确传参
+ def _try_json(txt: str):
+ try:
+ return json.loads(txt)
+ except Exception:
+ return None
+
+ obj = None
+ # 1a 单个对象
+ if raw.startswith("{") and raw.endswith("}"):
+ obj = _try_json(raw)
+ # 1b 常见模型输出尾部多了一个 ']' 的容错
+ if obj is None and raw.startswith("{") and raw.endswith("}]"):
+ obj = _try_json(raw[:-1].strip())
+ # 1c 模型输出为数组包裹一个对象
+ if obj is None and raw.startswith("[") and raw.endswith("]"):
+ arr = _try_json(raw)
+ if isinstance(arr, list) and len(arr) == 1 and isinstance(arr[0], dict):
+ obj = arr[0]
+ # 1d 错位尾括号(常见:{"a":1,"b":2}])
+ if obj is None and raw.endswith("}]") and raw.count("{") == 1 and raw.count("}") == 2:
+ obj = _try_json(raw[:-1])
+ # 1e 正则兜底:提取首个完整 JSON 对象
+ if obj is None and "{" in raw and "}" in raw:
+ try:
+ import re
+ # 使用括号匹配而非简单正则
+ def extract_first_json_object(text: str):
+ """从文本中提取第一个完整的 JSON 对象"""
+ start = text.find('{')
+ if start == -1:
+ return None
+ depth = 0
+ in_string = False
+ escape = False
+ for i, c in enumerate(text[start:], start):
+ if escape:
+ escape = False
+ continue
+ if c == '\\' and in_string:
+ escape = True
+ continue
+ if c == '"' and not escape:
+ in_string = not in_string
+ continue
+ if in_string:
+ continue
+ if c == '{':
+ depth += 1
+ elif c == '}':
+ depth -= 1
+ if depth == 0:
+ return text[start:i+1]
+ return None
+
+ json_str = extract_first_json_object(raw)
+ if json_str:
+ obj = json.loads(json_str)
+ except Exception:
+ pass
+
+ if isinstance(obj, dict):
+ safe_in = _scrub(obj)
+ result = tool.run(obj)
+ safe_out = _preview(result, limit=1600)
+ # 轻量 evidence:对常见工具提取关键字段,方便 UI 展示“证据来源”
+ evidence: dict[str, Any] = {}
+ try:
+ if name == "terminal":
+ cmd = safe_in.get("command") or safe_in.get("input")
+ if cmd:
+ evidence["command"] = cmd
+ if name == "context_fetch":
+ for k in ["sources", "query", "paths"]:
+ if k in safe_in:
+ evidence[k] = safe_in.get(k)
+ except Exception:
+ pass
+
+ log_event(
+ "tool",
+ {
+ "tool": name,
+ "tool_call_id": tool_call_id,
+ "ok": True,
+ "ms": int((time.time() - start) * 1000),
+ "input": safe_in,
+ "output_preview": safe_out,
+ "output_len": len((result or "").encode("utf-8", errors="ignore")) if isinstance(result, str) else None,
+ "evidence": evidence or None,
+ },
+ )
+ return result
+
+ # 2) 单参数兜底:如果工具只有一个必填参数,把 input_text 映射到该参数名
+ params = tool.get_parameters()
+ required = [p for p in params if p.required]
+ # 2a 无必填参数:允许空参数调用
+ if len(required) == 0:
+ result = tool.run({})
+ log_event(
+ "tool",
+ {
+ "tool": name,
+ "tool_call_id": tool_call_id,
+ "ok": True,
+ "ms": int((time.time() - start) * 1000),
+ "input": {},
+ "output_preview": _preview(result, limit=1600),
+ },
+ )
+ return result
+ if len(required) == 1:
+ in_obj = {required[0].name: input_text}
+ result = tool.run(in_obj)
+ log_event(
+ "tool",
+ {
+ "tool": name,
+ "tool_call_id": tool_call_id,
+ "ok": True,
+ "ms": int((time.time() - start) * 1000),
+ "input": _scrub(in_obj),
+ "output_preview": _preview(result, limit=1600),
+ },
+ )
+ return result
+
+ # 3) 兼容旧行为:若存在 input 参数,使用 input
+ if any(p.name == "input" for p in params):
+ in_obj = {"input": input_text}
+ result = tool.run(in_obj)
+ log_event(
+ "tool",
+ {
+ "tool": name,
+ "tool_call_id": tool_call_id,
+ "ok": True,
+ "ms": int((time.time() - start) * 1000),
+ "input": _scrub(in_obj),
+ "output_preview": _preview(result, limit=1600),
+ },
+ )
+ return result
+
+ return (
+ f"错误:工具 '{name}' 需要结构化参数。"
+ "请使用 JSON 形式传参,例如:tool[{\"param\":\"value\"}]"
+ )
+ except Exception as e:
+ log_event(
+ "tool",
+ {
+ "tool": name,
+ "tool_call_id": tool_call_id,
+ "ok": False,
+ "ms": int((time.time() - start) * 1000),
+ "input_preview": (raw[:600] + "...") if isinstance(raw, str) and len(raw) > 600 else raw,
+ "error": str(e),
+ },
+ )
+ return f"错误:执行工具 '{name}' 时发生异常: {str(e)}"
+
+ # 查找函数工具
+ elif name in self._functions:
+ func = self._functions[name]["func"]
+ try:
+ start = time.time()
+ tool_call_id = uuid.uuid4().hex[:12]
+ result = func(input_text)
+ log_event(
+ "tool",
+ {
+ "tool": name,
+ "tool_call_id": tool_call_id,
+ "ok": True,
+ "ms": int((time.time() - start) * 1000),
+ "input_preview": (input_text[:600] + "...") if isinstance(input_text, str) and len(input_text) > 600 else input_text,
+ "output_preview": (result[:1600] + "...") if isinstance(result, str) and len(result) > 1600 else result,
+ },
+ )
+ return result
+ except Exception as e:
+ log_event(
+ "tool",
+ {
+ "tool": name,
+ "tool_call_id": tool_call_id,
+ "ok": False,
+ "ms": int((time.time() - start) * 1000),
+ "error": str(e),
+ },
+ )
+ return f"错误:执行工具 '{name}' 时发生异常: {str(e)}"
+
+ else:
+ return f"错误:未找到名为 '{name}' 的工具。"
+
+ def get_tools_description(self) -> str:
+ """
+ 获取所有可用工具的格式化描述字符串
+
+ Returns:
+ 工具描述字符串,用于构建提示词
+ """
+ descriptions = []
+
+ # Tool对象描述
+ for tool in self._tools.values():
+ descriptions.append(f"- {tool.name}: {tool.description}")
+
+ # 函数工具描述
+ for name, info in self._functions.items():
+ descriptions.append(f"- {name}: {info['description']}")
+
+ return "\n".join(descriptions) if descriptions else "暂无可用工具"
+
+ def list_tools(self) -> list[str]:
+ """列出所有工具名称"""
+ return list(self._tools.keys()) + list(self._functions.keys())
+
+ def get_all_tools(self) -> list[Tool]:
+ """获取所有Tool对象"""
+ return list(self._tools.values())
+
+ def clear(self):
+ """清空所有工具"""
+ self._tools.clear()
+ self._functions.clear()
+ print("🧹 所有工具已清空。")
+
+# 全局工具注册表
+global_registry = ToolRegistry()
diff --git a/Co-creation-projects/aug618-Praxis/utils/__init__.py b/Co-creation-projects/aug618-Praxis/utils/__init__.py
new file mode 100644
index 00000000..112b04a7
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/__init__.py
@@ -0,0 +1,11 @@
+"""通用工具模块"""
+
+from .logging import setup_logger, get_logger
+from .serialization import serialize_object, deserialize_object
+from .helpers import format_time, validate_config, safe_import
+
+__all__ = [
+ "setup_logger", "get_logger",
+ "serialize_object", "deserialize_object",
+ "format_time", "validate_config", "safe_import"
+]
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/utils/cli_ui.py b/Co-creation-projects/aug618-Praxis/utils/cli_ui.py
new file mode 100644
index 00000000..bf687ce0
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/cli_ui.py
@@ -0,0 +1,86 @@
+"""CLI UI helpers (ANSI colors, spinner, consistent formatting).
+
+Kept dependency-free to avoid pulling extra packages into the MVP.
+"""
+
+from __future__ import annotations
+
+import os
+import sys
+import threading
+import time
+from typing import Iterable
+
+
+RESET = "\x1b[0m"
+PRIMARY = "\x1b[38;2;120;200;255m"
+ACCENT = "\x1b[38;2;150;140;255m"
+INFO = "\x1b[38;2;120;120;120m"
+WARN = "\x1b[38;2;255;190;120m"
+ERROR = "\x1b[38;2;255;120;120m"
+
+
+def supports_ansi() -> bool:
+ if os.getenv("NO_COLOR"):
+ return False
+ return sys.stdout.isatty()
+
+
+def c(text: str, color: str) -> str:
+ if not supports_ansi():
+ return text
+ return f"{color}{text}{RESET}"
+
+
+def hr(char: str = "=", width: int = 80) -> str:
+ return char * width
+
+
+def clamp_text(text: str, limit: int = 40_000) -> str:
+ if text is None:
+ return ""
+ if len(text) <= limit:
+ return text
+ return text[:limit] + f"\n\n..."
+
+
+def log_tool_event(name: str, message: str) -> None:
+ """LangChain-demo-like tool logging."""
+ header = f"⏺ {name}"
+ print(c(header, ACCENT))
+ lines: Iterable[str] = message.splitlines() if message else [""]
+ for line in lines:
+ print(f" ⎿ {line}")
+
+
+class Spinner:
+ """Simple terminal spinner."""
+
+ def __init__(self, label: str = "Thinking…") -> None:
+ self.label = label
+ self.frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
+ self._index = 0
+ self._running = False
+ self._thread: threading.Thread | None = None
+
+ def start(self) -> None:
+ if not supports_ansi() or self._running:
+ return
+ self._running = True
+
+ def _run() -> None:
+ while self._running:
+ frame = self.frames[self._index % len(self.frames)]
+ self._index += 1
+ print(c(f"{frame} {self.label}", INFO), end="\r", flush=True)
+ time.sleep(0.08)
+
+ self._thread = threading.Thread(target=_run, daemon=True)
+ self._thread.start()
+
+ def stop(self) -> None:
+ if not self._running:
+ return
+ self._running = False
+ if supports_ansi():
+ print("\r\x1b[2K", end="", flush=True)
diff --git a/Co-creation-projects/aug618-Praxis/utils/env.py b/Co-creation-projects/aug618-Praxis/utils/env.py
new file mode 100644
index 00000000..6d82df2f
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/env.py
@@ -0,0 +1,42 @@
+"""环境变量读取工具。
+
+统一处理布尔/字符串读取,确保全仓语义一致。
+"""
+
+from __future__ import annotations
+
+import os
+
+_TRUE_SET = {"1", "true", "yes", "y"}
+_FALSE_SET = {"0", "false", "no", "n"}
+
+
+def env_str(name: str, default: str = "") -> str:
+ """读取环境变量字符串(保留原始空白)。"""
+ return os.getenv(name, default)
+
+
+def env_stripped(name: str, default: str = "") -> str:
+ """读取环境变量并去除首尾空白。"""
+ return os.getenv(name, default).strip()
+
+
+def env_lower(name: str, default: str = "") -> str:
+ """读取环境变量并转为小写(含去空白)。"""
+ return os.getenv(name, default).strip().lower()
+
+
+def env_flag(name: str, default: bool = False) -> bool:
+ """读取布尔型环境变量(仅真值集合为 True)。"""
+ raw = os.getenv(name)
+ if raw is None:
+ return default
+ return raw.strip().lower() in _TRUE_SET
+
+
+def env_flag_true(name: str, default: bool = True) -> bool:
+ """读取布尔型环境变量(仅假值集合为 False)。"""
+ raw = os.getenv(name)
+ if raw is None:
+ return default
+ return raw.strip().lower() not in _FALSE_SET
diff --git a/Co-creation-projects/aug618-Praxis/utils/helpers.py b/Co-creation-projects/aug618-Praxis/utils/helpers.py
new file mode 100644
index 00000000..d5fd5d24
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/helpers.py
@@ -0,0 +1,75 @@
+"""辅助工具函数"""
+
+import importlib
+from datetime import datetime
+from typing import Any, Dict, Optional
+from pathlib import Path
+
+def format_time(timestamp: Optional[datetime] = None, format_str: str = "%Y-%m-%d %H:%M:%S") -> str:
+ """
+ 格式化时间
+
+ Args:
+ timestamp: 时间戳,默认为当前时间
+ format_str: 格式字符串
+
+ Returns:
+ 格式化后的时间字符串
+ """
+ if timestamp is None:
+ timestamp = datetime.now()
+ return timestamp.strftime(format_str)
+
+def validate_config(config: Dict[str, Any], required_keys: list) -> bool:
+ """
+ 验证配置是否包含必需的键
+
+ Args:
+ config: 配置字典
+ required_keys: 必需的键列表
+
+ Returns:
+ 是否验证通过
+ """
+ missing_keys = [key for key in required_keys if key not in config]
+ if missing_keys:
+ raise ValueError(f"配置缺少必需的键: {missing_keys}")
+ return True
+
+def safe_import(module_name: str, class_name: Optional[str] = None) -> Any:
+ """
+ 安全导入模块或类
+
+ Args:
+ module_name: 模块名
+ class_name: 类名(可选)
+
+ Returns:
+ 导入的模块或类
+ """
+ try:
+ module = importlib.import_module(module_name)
+ if class_name:
+ return getattr(module, class_name)
+ return module
+ except (ImportError, AttributeError) as e:
+ raise ImportError(f"无法导入 {module_name}.{class_name or ''}: {e}")
+
+def ensure_dir(path: Path) -> Path:
+ """确保目录存在"""
+ path.mkdir(parents=True, exist_ok=True)
+ return path
+
+def get_project_root() -> Path:
+ """获取项目根目录"""
+ return Path(__file__).parent.parent.parent
+
+def merge_dicts(dict1: Dict, dict2: Dict) -> Dict:
+ """深度合并两个字典"""
+ result = dict1.copy()
+ for key, value in dict2.items():
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
+ result[key] = merge_dicts(result[key], value)
+ else:
+ result[key] = value
+ return result
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/utils/logging.py b/Co-creation-projects/aug618-Praxis/utils/logging.py
new file mode 100644
index 00000000..140f69cc
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/logging.py
@@ -0,0 +1,39 @@
+"""日志工具"""
+
+import logging
+import sys
+from typing import Optional
+
+def setup_logger(
+ name: str = "hello_agents",
+ level: str = "INFO",
+ format_string: Optional[str] = None
+) -> logging.Logger:
+ """
+ 设置日志记录器
+
+ Args:
+ name: 日志记录器名称
+ level: 日志级别
+ format_string: 日志格式
+
+ Returns:
+ 配置好的日志记录器
+ """
+ logger = logging.getLogger(name)
+ logger.setLevel(getattr(logging, level.upper()))
+
+ if not logger.handlers:
+ handler = logging.StreamHandler(sys.stdout)
+ formatter = logging.Formatter(
+ format_string or
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+ )
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+
+ return logger
+
+def get_logger(name: str = "hello_agents") -> logging.Logger:
+ """获取日志记录器"""
+ return logging.getLogger(name)
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/utils/multimodal.py b/Co-creation-projects/aug618-Praxis/utils/multimodal.py
new file mode 100644
index 00000000..eb59fc28
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/multimodal.py
@@ -0,0 +1,43 @@
+"""Multimodal helpers (image -> OpenAI-compatible message parts).
+
+Keep it dependency-free (no Pillow) so it works in minimal environments.
+"""
+
+from __future__ import annotations
+
+import base64
+import mimetypes
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+
+def encode_image_to_data_url(path: str | Path, mime_type: Optional[str] = None) -> str:
+ """Encode a local image file to a data URL (data:;base64,...)."""
+ p = Path(path).expanduser().resolve()
+ if not p.exists() or not p.is_file():
+ raise FileNotFoundError(f"Image not found: {p}")
+
+ mt = mime_type
+ if not mt:
+ mt, _ = mimetypes.guess_type(str(p))
+ if not mt:
+ # Safe default; most providers accept image/jpeg or image/png
+ mt = "image/jpeg"
+
+ b64 = base64.b64encode(p.read_bytes()).decode("utf-8")
+ return f"data:{mt};base64,{b64}"
+
+
+def image_part_from_path(path: str | Path, mime_type: Optional[str] = None) -> Dict[str, Any]:
+ """Build an OpenAI-compatible image part from a local file path."""
+ data_url = encode_image_to_data_url(path, mime_type=mime_type)
+ return {"type": "image_url", "image_url": {"url": data_url}}
+
+
+def build_user_content_with_images(text: str, image_paths: List[str | Path]) -> List[Dict[str, Any]]:
+ """Build a user message content list: text + N image parts."""
+ parts: List[Dict[str, Any]] = [{"type": "text", "text": text}]
+ for p in image_paths:
+ parts.append(image_part_from_path(p))
+ return parts
+
diff --git a/Co-creation-projects/aug618-Praxis/utils/observability.py b/Co-creation-projects/aug618-Praxis/utils/observability.py
new file mode 100644
index 00000000..54d3d248
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/observability.py
@@ -0,0 +1,68 @@
+"""轻量可观测性:JSONL 事件日志"""
+
+from __future__ import annotations
+
+import json
+import time
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any, Dict, Optional
+
+import tiktoken
+
+from utils.env import env_str
+
+
+def _now_iso() -> str:
+ return datetime.now(timezone.utc).isoformat()
+
+
+def _resolve_log_path() -> Path:
+ log_dir = env_str("CODE_AGENT_LOG_DIR")
+ if log_dir:
+ return Path(log_dir).expanduser().resolve() / "events.jsonl"
+ # 兜底:当前工作目录下的 .helloagents/logs
+ return Path.cwd() / ".helloagents" / "logs" / "events.jsonl"
+
+
+def log_event(event_type: str, data: Dict[str, Any]) -> None:
+ """写入结构化事件到 JSONL。失败时静默。"""
+ try:
+ path = _resolve_log_path()
+ path.parent.mkdir(parents=True, exist_ok=True)
+ session_id = env_str("CODE_AGENT_SESSION_ID")
+ base = {"ts": _now_iso(), "type": event_type}
+ if session_id:
+ base["session_id"] = session_id
+ payload = {**base, **data}
+ with path.open("a", encoding="utf-8") as f:
+ f.write(json.dumps(payload, ensure_ascii=False) + "\n")
+ except Exception:
+ # 避免日志失败影响主流程
+ return
+
+
+def _count_tokens(text: str) -> int:
+ try:
+ encoding = tiktoken.get_encoding("cl100k_base")
+ return len(encoding.encode(text))
+ except Exception:
+ return len(text) // 4
+
+
+def estimate_prompt_tokens(messages: list[dict[str, Any]]) -> int:
+ """粗略估算 prompt tokens(适用于多模态 content)。"""
+ try:
+ return _count_tokens(json.dumps(messages, ensure_ascii=False))
+ except Exception:
+ return 0
+
+
+def estimate_completion_tokens(text: Optional[str]) -> int:
+ if not text:
+ return 0
+ return _count_tokens(text)
+
+
+def measure_ms(start_time: float) -> int:
+ return int((time.time() - start_time) * 1000)
diff --git a/Co-creation-projects/aug618-Praxis/utils/patch_utils.py b/Co-creation-projects/aug618-Praxis/utils/patch_utils.py
new file mode 100644
index 00000000..9e521ed7
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/patch_utils.py
@@ -0,0 +1,49 @@
+"""补丁处理工具(与 CLI/TUI 共用)。"""
+
+from __future__ import annotations
+
+import re
+
+# 匹配 Codex 风格补丁块(宽松,跨行,允许前导空白或代码围栏)
+PATCH_RE = re.compile(r"\s*\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch", re.MULTILINE)
+# 备用:从 ```patch/```diff 围栏中提取补丁主体
+PATCH_FENCE_RE = re.compile(
+ r"```(?:patch|diff|text)?\s*(\*\*\* Begin Patch[\s\S]*?\*\*\* End Patch)\s*```",
+ re.MULTILINE,
+)
+
+
+def extract_patch(text: str) -> str | None:
+ """从文本中提取补丁块。"""
+ m = PATCH_FENCE_RE.search(text)
+ if m:
+ return m.group(1)
+ m = PATCH_RE.search(text)
+ return m.group(0).strip() if m else None
+
+
+def normalize_patch(patch_text: str) -> str:
+ """规范化补丁格式(容错处理模型输出)。"""
+ lines = patch_text.splitlines()
+ out: list[str] = []
+ for line in lines:
+ stripped = line.strip()
+ if stripped.startswith(("Add File:", "Update File:", "Delete File:")) and not stripped.startswith("*** "):
+ out.append("*** " + stripped)
+ continue
+ out.append(line)
+ return "\n".join(out)
+
+
+def patch_requires_confirmation(patch_text: str) -> bool:
+ """判断补丁是否需要用户确认。"""
+ if "*** Delete File:" in patch_text:
+ return True
+ file_ops = patch_text.count("*** Add File:") + patch_text.count("*** Update File:") + patch_text.count("*** Delete File:")
+ if file_ops >= 6:
+ return True
+ changed_lines = 0
+ for line in patch_text.splitlines():
+ if line.startswith("+") or line.startswith("-"):
+ changed_lines += 1
+ return changed_lines >= 400
diff --git a/Co-creation-projects/aug618-Praxis/utils/references.py b/Co-creation-projects/aug618-Praxis/utils/references.py
new file mode 100644
index 00000000..18199830
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/references.py
@@ -0,0 +1,402 @@
+"""Reference parser for @ syntax.
+
+Supports:
+- @path/to/file.py - file reference (simplified syntax)
+- @src/ - directory reference (trailing slash)
+- @file(path) - legacy file syntax (still supported)
+- @dir(path) - legacy dir syntax (still supported)
+- Multiple references: @core/llm.py @utils/ 比较这两个
+
+Image extensions are treated as multimodal attachments.
+Text/code files are injected into context.
+"""
+
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple
+
+from .multimodal import image_part_from_path
+
+# Image extensions that should be sent as multimodal attachments
+IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".tif"}
+
+# Text/code extensions we can safely read
+TEXT_EXTENSIONS = {
+ ".py", ".js", ".ts", ".jsx", ".tsx", ".java", ".c", ".cpp", ".h", ".hpp",
+ ".go", ".rs", ".rb", ".php", ".swift", ".kt", ".scala", ".cs",
+ ".html", ".css", ".scss", ".less", ".vue", ".svelte",
+ ".json", ".yaml", ".yml", ".toml", ".xml", ".ini", ".cfg",
+ ".md", ".txt", ".rst", ".tex", ".log",
+ ".sh", ".bash", ".zsh", ".fish", ".ps1", ".bat", ".cmd",
+ ".sql", ".graphql", ".proto",
+ ".dockerfile", ".gitignore", ".env.example",
+}
+
+# Max file size to read (prevent huge files from blowing up context)
+MAX_FILE_SIZE = 100 * 1024 # 100KB
+MAX_DIR_FILES = 20 # Max files to include from a directory
+MAX_DIR_DEPTH = 3 # Max depth for directory traversal
+
+
+@dataclass
+class ParsedReferences:
+ """Result of parsing @file/@dir references from user input."""
+ clean_query: str # User query with references removed
+ image_attachments: List[Dict[str, Any]] = field(default_factory=list)
+ image_paths: List[Path] = field(default_factory=list) # 原始图片路径(用于 OCR)
+ context_blocks: List[str] = field(default_factory=list)
+ errors: List[str] = field(default_factory=list)
+
+
+def parse_references(
+ user_input: str,
+ workspace: str | Path,
+) -> ParsedReferences:
+ """
+ Parse @ references from user input.
+
+ Supports two syntaxes:
+ 1. Simplified: @path/to/file.py or @src/ (directory with trailing /)
+ 2. Legacy: @file(path) or @dir(path)
+
+ Args:
+ user_input: Raw user input with potential @ references
+ workspace: Base directory for resolving relative paths
+
+ Returns:
+ ParsedReferences with attachments, context blocks, and cleaned query
+ """
+ workspace = Path(workspace).resolve()
+ result = ParsedReferences(clean_query=user_input)
+
+ # Track all matched spans to remove later
+ remove_spans: List[Tuple[int, int]] = []
+
+ # Pattern 1: Legacy @file(paths) or @dir(paths)
+ legacy_pattern = r'@(file|dir)\(([^)]+)\)'
+ for match in re.finditer(legacy_pattern, user_input, re.IGNORECASE):
+ remove_spans.append((match.start(), match.end()))
+ ref_type = match.group(1).lower()
+ paths_str = match.group(2).strip()
+ paths = _split_paths(paths_str)
+
+ for ref_path in paths:
+ ref_path = ref_path.strip().strip("\"'")
+ if not ref_path:
+ continue
+ full_path = (workspace / ref_path).resolve()
+ try:
+ full_path.relative_to(workspace)
+ except ValueError:
+ result.errors.append(f"路径不在工作目录内: {ref_path}")
+ continue
+
+ if ref_type == "file":
+ _process_file(full_path, ref_path, result)
+ else:
+ _process_dir(full_path, ref_path, workspace, result)
+
+ # Pattern 2: Simplified @path (not followed by file/dir parenthesis)
+ # Match @followed by path characters until whitespace or end
+ # Path can contain: letters, numbers, /, ., _, -, but not @
+ simple_pattern = r'@(?!file\(|dir\()([a-zA-Z0-9_./-]+)'
+ for match in re.finditer(simple_pattern, user_input):
+ # Skip if this overlaps with a legacy match
+ start, end = match.start(), match.end()
+ if any(s <= start < e or s < end <= e for s, e in remove_spans):
+ continue
+
+ remove_spans.append((start, end))
+ ref_path = match.group(1).strip()
+ if not ref_path:
+ continue
+
+ full_path = (workspace / ref_path).resolve()
+ try:
+ full_path.relative_to(workspace)
+ except ValueError:
+ result.errors.append(f"路径不在工作目录内: {ref_path}")
+ continue
+
+ # Auto-detect: directory (ends with / or is a dir) vs file
+ if ref_path.endswith("/") or full_path.is_dir():
+ _process_dir(full_path, ref_path, workspace, result)
+ else:
+ _process_file(full_path, ref_path, result)
+
+ # Remove all references from the query
+ # Sort spans in reverse order to remove from end first
+ remove_spans.sort(reverse=True)
+ clean = user_input
+ for start, end in remove_spans:
+ clean = clean[:start] + clean[end:]
+
+ # Clean up extra whitespace
+ result.clean_query = " ".join(clean.split()).strip()
+
+ # If query is empty after removing references, provide a default
+ if not result.clean_query:
+ if result.image_attachments:
+ result.clean_query = "请描述这些图片的内容"
+ elif result.context_blocks:
+ result.clean_query = "请分析这些文件/目录的内容"
+
+ return result
+
+
+def _split_paths(paths_str: str) -> List[str]:
+ """
+ Split a paths string by comma, Chinese comma, or semicolon.
+ Handles quoted paths with spaces.
+
+ Examples:
+ "a.py, b.py" -> ["a.py", "b.py"]
+ "a.py、b.py" -> ["a.py", "b.py"]
+ '"path with spaces/a.py", b.py' -> ["path with spaces/a.py", "b.py"]
+ """
+ # Separators: comma, Chinese comma, semicolon
+ separators = [",", "、", ";", ";"]
+
+ # Simple case: no quotes
+ if '"' not in paths_str and "'" not in paths_str:
+ for sep in separators:
+ if sep in paths_str:
+ return [p.strip() for p in paths_str.split(sep) if p.strip()]
+ # No separator found, single path
+ return [paths_str.strip()] if paths_str.strip() else []
+
+ # Complex case: handle quotes
+ paths: List[str] = []
+ current = ""
+ in_quote = None
+
+ for char in paths_str:
+ if char in ('"', "'") and in_quote is None:
+ in_quote = char
+ elif char == in_quote:
+ in_quote = None
+ elif char in separators and in_quote is None:
+ if current.strip():
+ paths.append(current.strip().strip("\"'"))
+ current = ""
+ continue
+ current += char
+
+ if current.strip():
+ paths.append(current.strip().strip("\"'"))
+
+ return paths
+
+
+def _process_file(full_path: Path, display_path: str, result: ParsedReferences) -> None:
+ """Process a single file reference."""
+ if not full_path.exists():
+ result.errors.append(f"文件不存在: {display_path}")
+ return
+
+ if not full_path.is_file():
+ result.errors.append(f"不是文件: {display_path}")
+ return
+
+ suffix = full_path.suffix.lower()
+
+ # Image file -> multimodal attachment + save path for OCR fallback
+ if suffix in IMAGE_EXTENSIONS:
+ try:
+ attachment = image_part_from_path(full_path)
+ result.image_attachments.append(attachment)
+ result.image_paths.append(full_path) # 保存原始路径,用于 OCR 降级
+ result.context_blocks.append(f"[图片: {display_path}]")
+ except Exception as e:
+ result.errors.append(f"无法读取图片 {display_path}: {e}")
+ return
+
+ # Text/code file -> context block
+ if suffix in TEXT_EXTENSIONS or suffix == "" or _is_likely_text(full_path):
+ try:
+ size = full_path.stat().st_size
+ if size > MAX_FILE_SIZE:
+ content = full_path.read_text(encoding="utf-8", errors="ignore")[:MAX_FILE_SIZE]
+ content += f"\n\n... (文件过大,已截断,原始大小: {size} bytes)"
+ else:
+ content = full_path.read_text(encoding="utf-8", errors="ignore")
+
+ block = f"--- @file({display_path}) ---\n```{_get_language(suffix)}\n{content}\n```"
+ result.context_blocks.append(block)
+ except Exception as e:
+ result.errors.append(f"无法读取文件 {display_path}: {e}")
+ return
+
+ # Unknown extension - try to read as text
+ try:
+ content = full_path.read_text(encoding="utf-8", errors="ignore")
+ if len(content) > MAX_FILE_SIZE:
+ content = content[:MAX_FILE_SIZE] + "\n\n... (已截断)"
+ block = f"--- @file({display_path}) ---\n```\n{content}\n```"
+ result.context_blocks.append(block)
+ except Exception:
+ result.errors.append(f"无法读取文件(可能是二进制): {display_path}")
+
+
+def _process_dir(
+ full_path: Path,
+ display_path: str,
+ workspace: Path,
+ result: ParsedReferences,
+) -> None:
+ """Process a directory reference."""
+ if not full_path.exists():
+ result.errors.append(f"目录不存在: {display_path}")
+ return
+
+ if not full_path.is_dir():
+ result.errors.append(f"不是目录: {display_path}")
+ return
+
+ # Build directory tree
+ tree_lines = [f"--- @dir({display_path}) ---", "目录结构:"]
+ tree_lines.extend(_build_tree(full_path, prefix="", depth=0, max_depth=MAX_DIR_DEPTH))
+
+ # Collect key files to include content
+ key_files: List[Path] = []
+ for f in _iter_files(full_path, max_depth=MAX_DIR_DEPTH):
+ if len(key_files) >= MAX_DIR_FILES:
+ break
+ suffix = f.suffix.lower()
+ if suffix in TEXT_EXTENSIONS or f.name in {"Makefile", "Dockerfile", "README", "LICENSE"}:
+ key_files.append(f)
+
+ # Add file contents
+ file_blocks: List[str] = []
+ for f in key_files[:MAX_DIR_FILES]:
+ try:
+ rel = f.relative_to(full_path)
+ size = f.stat().st_size
+ if size > MAX_FILE_SIZE // 2: # Smaller limit for dir files
+ content = f.read_text(encoding="utf-8", errors="ignore")[:MAX_FILE_SIZE // 2]
+ content += "\n... (已截断)"
+ else:
+ content = f.read_text(encoding="utf-8", errors="ignore")
+
+ lang = _get_language(f.suffix.lower())
+ file_blocks.append(f"## {rel}\n```{lang}\n{content}\n```")
+ except Exception:
+ continue
+
+ # Combine tree + files
+ block = "\n".join(tree_lines)
+ if file_blocks:
+ block += "\n\n关键文件内容:\n" + "\n\n".join(file_blocks)
+
+ result.context_blocks.append(block)
+
+
+def _build_tree(path: Path, prefix: str, depth: int, max_depth: int) -> List[str]:
+ """Build a tree representation of a directory."""
+ if depth >= max_depth:
+ return [f"{prefix}... (deeper levels omitted)"]
+
+ lines: List[str] = []
+ try:
+ entries = sorted(path.iterdir(), key=lambda x: (x.is_file(), x.name.lower()))
+ except PermissionError:
+ return [f"{prefix}(permission denied)"]
+
+ # Filter out hidden and common ignored dirs
+ ignored = {".git", ".hg", ".svn", "__pycache__", "node_modules", ".venv", "venv", ".idea", ".vscode"}
+ entries = [e for e in entries if not e.name.startswith(".") or e.name in {".gitignore", ".env.example"}]
+ entries = [e for e in entries if e.name not in ignored]
+
+ for i, entry in enumerate(entries[:30]): # Limit entries per level
+ is_last = i == len(entries) - 1 or i == 29
+ connector = "└── " if is_last else "├── "
+
+ if entry.is_dir():
+ lines.append(f"{prefix}{connector}{entry.name}/")
+ extension = " " if is_last else "│ "
+ lines.extend(_build_tree(entry, prefix + extension, depth + 1, max_depth))
+ else:
+ lines.append(f"{prefix}{connector}{entry.name}")
+
+ if len(entries) > 30:
+ lines.append(f"{prefix}... ({len(entries) - 30} more items)")
+
+ return lines
+
+
+def _iter_files(path: Path, max_depth: int, current_depth: int = 0) -> List[Path]:
+ """Iterate files in a directory up to max_depth."""
+ if current_depth >= max_depth:
+ return []
+
+ files: List[Path] = []
+ try:
+ for entry in path.iterdir():
+ if entry.name.startswith(".") and entry.name not in {".gitignore", ".env.example"}:
+ continue
+ if entry.name in {"node_modules", "__pycache__", ".git", "venv", ".venv"}:
+ continue
+ if entry.is_file():
+ files.append(entry)
+ elif entry.is_dir():
+ files.extend(_iter_files(entry, max_depth, current_depth + 1))
+ except PermissionError:
+ pass
+ return files
+
+
+def _is_likely_text(path: Path) -> bool:
+ """Heuristic: check if file is likely text by reading first bytes."""
+ try:
+ with open(path, "rb") as f:
+ chunk = f.read(1024)
+ # If it contains null bytes, probably binary
+ return b"\x00" not in chunk
+ except Exception:
+ return False
+
+
+def _get_language(suffix: str) -> str:
+ """Map file suffix to markdown code block language."""
+ mapping = {
+ ".py": "python",
+ ".js": "javascript",
+ ".ts": "typescript",
+ ".jsx": "jsx",
+ ".tsx": "tsx",
+ ".java": "java",
+ ".c": "c",
+ ".cpp": "cpp",
+ ".h": "c",
+ ".hpp": "cpp",
+ ".go": "go",
+ ".rs": "rust",
+ ".rb": "ruby",
+ ".php": "php",
+ ".swift": "swift",
+ ".kt": "kotlin",
+ ".scala": "scala",
+ ".cs": "csharp",
+ ".html": "html",
+ ".css": "css",
+ ".scss": "scss",
+ ".less": "less",
+ ".vue": "vue",
+ ".svelte": "svelte",
+ ".json": "json",
+ ".yaml": "yaml",
+ ".yml": "yaml",
+ ".toml": "toml",
+ ".xml": "xml",
+ ".md": "markdown",
+ ".sh": "bash",
+ ".bash": "bash",
+ ".zsh": "zsh",
+ ".sql": "sql",
+ ".graphql": "graphql",
+ ".proto": "protobuf",
+ }
+ return mapping.get(suffix, "")
diff --git a/Co-creation-projects/aug618-Praxis/utils/serialization.py b/Co-creation-projects/aug618-Praxis/utils/serialization.py
new file mode 100644
index 00000000..adfb09e0
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/serialization.py
@@ -0,0 +1,61 @@
+"""序列化工具"""
+
+import json
+import pickle
+from typing import Any, Union
+from pathlib import Path
+
+def serialize_object(obj: Any, format: str = "json") -> Union[str, bytes]:
+ """
+ 序列化对象
+
+ Args:
+ obj: 要序列化的对象
+ format: 序列化格式 ("json" 或 "pickle")
+
+ Returns:
+ 序列化后的数据
+ """
+ if format == "json":
+ return json.dumps(obj, ensure_ascii=False, indent=2)
+ elif format == "pickle":
+ return pickle.dumps(obj)
+ else:
+ raise ValueError(f"不支持的序列化格式: {format}")
+
+def deserialize_object(data: Union[str, bytes], format: str = "json") -> Any:
+ """
+ 反序列化对象
+
+ Args:
+ data: 序列化的数据
+ format: 序列化格式
+
+ Returns:
+ 反序列化后的对象
+ """
+ if format == "json":
+ return json.loads(data)
+ elif format == "pickle":
+ return pickle.loads(data)
+ else:
+ raise ValueError(f"不支持的反序列化格式: {format}")
+
+def save_to_file(obj: Any, filepath: Union[str, Path], format: str = "json") -> None:
+ """保存对象到文件"""
+ filepath = Path(filepath)
+ data = serialize_object(obj, format)
+
+ mode = "w" if format == "json" else "wb"
+ with open(filepath, mode) as f:
+ f.write(data)
+
+def load_from_file(filepath: Union[str, Path], format: str = "json") -> Any:
+ """从文件加载对象"""
+ filepath = Path(filepath)
+ mode = "r" if format == "json" else "rb"
+
+ with open(filepath, mode) as f:
+ data = f.read()
+
+ return deserialize_object(data, format)
\ No newline at end of file
diff --git a/Co-creation-projects/aug618-Praxis/utils/session_utils.py b/Co-creation-projects/aug618-Praxis/utils/session_utils.py
new file mode 100644
index 00000000..3c6c295c
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/session_utils.py
@@ -0,0 +1,91 @@
+"""会话/事件日志处理工具(与 CLI/TUI 共用)。"""
+
+from __future__ import annotations
+
+import json
+from datetime import datetime
+from pathlib import Path
+
+
+def load_events(log_path: Path) -> list[dict]:
+ """从 JSONL 读取事件列表。"""
+ if not log_path.exists():
+ return []
+ events: list[dict] = []
+ with log_path.open("r", encoding="utf-8") as f:
+ for line in f:
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ events.append(json.loads(line))
+ except Exception:
+ continue
+ return events
+
+
+def parse_ts(ts: str) -> datetime | None:
+ """解析 ISO 时间戳字符串。"""
+ try:
+ return datetime.fromisoformat(ts.replace("Z", "+00:00"))
+ except Exception:
+ return None
+
+
+def summarize_session(events: list[dict]) -> dict:
+ """汇总会话统计信息。"""
+ stats = {
+ "turns": 0,
+ "tool_calls": 0,
+ "tool_errors": 0,
+ "llm_calls": 0,
+ "llm_errors": 0,
+ "prompt_tokens": 0,
+ "completion_tokens": 0,
+ "prompt_tokens_est": 0,
+ "completion_tokens_est": 0,
+ "duration_ms": None,
+ "start_ts": None,
+ "end_ts": None,
+ }
+ for e in events:
+ et = e.get("type")
+ if et == "session_start":
+ stats["start_ts"] = e.get("ts")
+ elif et == "session_end":
+ stats["end_ts"] = e.get("ts")
+ stats["turns"] = e.get("turns", stats["turns"])
+ elif et == "tool":
+ stats["tool_calls"] += 1
+ if not e.get("ok", True):
+ stats["tool_errors"] += 1
+ elif et == "llm":
+ stats["llm_calls"] += 1
+ if not e.get("ok", True):
+ stats["llm_errors"] += 1
+ stats["prompt_tokens"] += e.get("prompt_tokens") or 0
+ stats["completion_tokens"] += e.get("completion_tokens") or 0
+ stats["prompt_tokens_est"] += e.get("prompt_tokens_est") or 0
+ stats["completion_tokens_est"] += e.get("completion_tokens_est") or 0
+
+ if stats["start_ts"] and stats["end_ts"]:
+ start_dt = parse_ts(stats["start_ts"])
+ end_dt = parse_ts(stats["end_ts"])
+ if start_dt and end_dt:
+ stats["duration_ms"] = int((end_dt - start_dt).total_seconds() * 1000)
+ return stats
+
+
+def export_session(session_id: str, events: list[dict], export_dir: Path) -> Path:
+ """导出会话事件为 JSON 文件。"""
+ export_dir.mkdir(parents=True, exist_ok=True)
+ summary = summarize_session(events)
+ payload = {
+ "session_id": session_id,
+ "summary": summary,
+ "events": events,
+ }
+ export_path = export_dir / f"session_{session_id}.json"
+ with export_path.open("w", encoding="utf-8") as f:
+ json.dump(payload, f, ensure_ascii=False, indent=2)
+ return export_path
diff --git a/Co-creation-projects/aug618-Praxis/utils/tui_ui.py b/Co-creation-projects/aug618-Praxis/utils/tui_ui.py
new file mode 100644
index 00000000..8b4cfe2d
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/utils/tui_ui.py
@@ -0,0 +1,152 @@
+"""TUI UI 工具(CSS 样式与公共工具引用)。
+
+与 cli_ui 的职责保持一致:仅提供样式与可复用的轻量工具入口。
+"""
+
+from __future__ import annotations
+
+# 复用 CLI/TUI 共用的补丁与会话工具,避免重复实现
+from utils.patch_utils import extract_patch, normalize_patch, patch_requires_confirmation
+from utils.session_utils import load_events, summarize_session, export_session
+
+
+# ============================================================
+# CSS Styles for Textual App
+# ============================================================
+
+TUI_CSS = """
+Screen {
+ layout: vertical;
+ background: #0f1115;
+}
+
+Header {
+ background: #141824;
+ color: #e8e8e8;
+}
+
+/* Footer widget removed; we use a minimal footer_bar */
+
+#logo {
+ height: auto;
+ max-height: 100;
+ background: #0f1115;
+ margin: 1 2 0 2;
+ content-align: center middle;
+}
+
+#trace {
+ height: auto;
+ max-height: 12;
+ background: #0f1115;
+ border: tall #202637;
+ margin: 0 2;
+ padding: 0 1;
+ display: none;
+}
+
+#output {
+ height: 1fr;
+ background: #0f1115;
+ border: tall #202637;
+ padding: 1 2;
+ scrollbar-gutter: stable;
+ overflow-y: auto;
+}
+
+Collapsible {
+ border: tall #202637;
+ margin: 0 2;
+}
+
+#suggestions {
+ height: auto;
+ max-height: 10;
+ background: #141824;
+ border: tall #202637;
+ margin: 0 2;
+ display: none;
+}
+
+#suggestions > ListItem {
+ padding: 0 2;
+ background: #141824;
+}
+
+#suggestions > ListItem:hover {
+ background: #202637;
+}
+
+#suggestions > ListItem.-highlight {
+ background: #4c7dff;
+ color: #0f1115;
+}
+
+#input_area {
+ dock: bottom;
+ height: 3;
+ background: #0f1115;
+ width: 1fr;
+}
+
+#input_line_top, #input_line_bottom {
+ height: 1;
+ /* Textual CSS 不支持 linear-gradient;渐变线由代码用 Rich Text 渲染 */
+ background: #0f1115;
+ width: 1fr;
+ content-align: left middle;
+}
+
+#input_row {
+ height: 1;
+ background: #0f1115;
+ padding: 0 2;
+}
+
+#input_prompt {
+ width: 2;
+ color: #7aa2f7;
+ text-style: bold;
+ content-align: left middle;
+}
+
+#input_bar {
+ background: #0f1115;
+ border: none;
+ padding: 0;
+ height: 1;
+}
+
+Input {
+ background: #0f1115;
+ border: none;
+ color: #e8e8e8;
+}
+
+Input:focus {
+ border: none;
+}
+
+/* Cursor shape is terminal-dependent; we can only style colors here */
+Input > .input--cursor {
+ background: #00ffff;
+ /* 某些终端/渲染环境可能不会正确绘制 cursor 的 background,
+ 若此时把 cursor 字符设为深色,会导致“光标所在字符消失”,看起来像输入乱码/缺字。
+ 这里用高对比亮色,保证无论背景是否生效都可见。 */
+ color: #e8e8e8;
+}
+
+/* 在部分终端里,Input 聚焦时的选区/占位符默认样式会显得像“乱码色块”。
+ 这里显式设置占位符与选区颜色,避免高对比的黄色块。 */
+Input.-placeholder {
+ color: #565f89;
+}
+
+Input > .input--selection {
+ background: #202637;
+ color: #e8e8e8;
+}
+
+/* footer_bar removed */
+"""
+
diff --git a/Co-creation-projects/aug618-Praxis/uv.lock b/Co-creation-projects/aug618-Praxis/uv.lock
new file mode 100644
index 00000000..093b3bcd
--- /dev/null
+++ b/Co-creation-projects/aug618-Praxis/uv.lock
@@ -0,0 +1,4522 @@
+version = 1
+revision = 3
+requires-python = ">=3.12"
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'linux'",
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.13.*' and sys_platform == 'linux'",
+ "python_full_version == '3.13.*' and sys_platform == 'win32'",
+ "python_full_version == '3.13.*' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.13' and sys_platform == 'linux'",
+ "python_full_version < '3.13' and sys_platform == 'win32'",
+ "python_full_version < '3.13' and sys_platform != 'linux' and sys_platform != 'win32'",
+]
+
+[[package]]
+name = "a2a-sdk"
+version = "0.3.22"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "google-api-core" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "protobuf" },
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/92/a3/76f2d94a32a1b0dc760432d893a09ec5ed31de5ad51b1ef0f9d199ceb260/a2a_sdk-0.3.22.tar.gz", hash = "sha256:77a5694bfc4f26679c11b70c7f1062522206d430b34bc1215cfbb1eba67b7e7d", size = 231535, upload-time = "2025-12-16T18:39:21.19Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/e8/f4e39fd1cf0b3c4537b974637143f3ebfe1158dad7232d9eef15666a81ba/a2a_sdk-0.3.22-py3-none-any.whl", hash = "sha256:b98701135bb90b0ff85d35f31533b6b7a299bf810658c1c65f3814a6c15ea385", size = 144347, upload-time = "2025-12-16T18:39:19.218Z" },
+]
+
+[[package]]
+name = "absl-py"
+version = "2.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" },
+]
+
+[[package]]
+name = "accelerate"
+version = "1.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "psutil" },
+ { name = "pyyaml" },
+ { name = "safetensors" },
+ { name = "torch" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" },
+]
+
+[[package]]
+name = "aiofiles"
+version = "23.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/41/cfed10bc64d774f497a86e5ede9248e1d062db675504b41c320954d99641/aiofiles-23.2.1.tar.gz", hash = "sha256:84ec2218d8419404abcb9f0c02df3f34c6e0a68ed41072acfb1cef5cbc29051a", size = 32072, upload-time = "2023-08-09T15:23:11.564Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c5/19/5af6804c4cc0fed83f47bff6e413a98a36618e7d40185cd36e69737f3b0e/aiofiles-23.2.1-py3-none-any.whl", hash = "sha256:19297512c647d4b27a2cf7c34caa7e405c0d60b5560618a29a9fe027b18b0107", size = 15727, upload-time = "2023-08-09T15:23:09.774Z" },
+]
+
+[[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.13.3"
+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/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732, upload-time = "2026-01-03T17:30:14.23Z" },
+ { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293, upload-time = "2026-01-03T17:30:15.96Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533, upload-time = "2026-01-03T17:30:17.431Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839, upload-time = "2026-01-03T17:30:19.422Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932, upload-time = "2026-01-03T17:30:21.756Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906, upload-time = "2026-01-03T17:30:23.932Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020, upload-time = "2026-01-03T17:30:26Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181, upload-time = "2026-01-03T17:30:27.554Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794, upload-time = "2026-01-03T17:30:29.254Z" },
+ { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900, upload-time = "2026-01-03T17:30:31.033Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239, upload-time = "2026-01-03T17:30:32.703Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527, upload-time = "2026-01-03T17:30:34.695Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489, upload-time = "2026-01-03T17:30:36.864Z" },
+ { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852, upload-time = "2026-01-03T17:30:39.433Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379, upload-time = "2026-01-03T17:30:41.081Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" },
+ { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" },
+ { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" },
+ { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
+ { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
+ { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
+ { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
+ { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
+ { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
+ { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
+ { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
+ { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
+ { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
+ { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
+ { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
+ { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
+ { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
+ { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
+ { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
+]
+
+[[package]]
+name = "aiosignal"
+version = "1.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "frozenlist" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
+]
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.12.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
+]
+
+[[package]]
+name = "attrs"
+version = "25.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
+]
+
+[[package]]
+name = "authlib"
+version = "1.6.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" },
+]
+
+[[package]]
+name = "beautifulsoup4"
+version = "4.14.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "soupsieve" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
+]
+
+[[package]]
+name = "bitsandbytes"
+version = "0.49.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "torch" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/19/6f/32d0526e4e4ad309d9e7502c018399bb23b63f39277a361c305092e2f885/bitsandbytes-0.49.1-py3-none-macosx_14_0_arm64.whl", hash = "sha256:9de01d4384b6c71ef9ab052b98457dc0e4fff8fe06ab14833b5b712700deb005", size = 129848, upload-time = "2026-01-08T14:31:26.134Z" },
+ { url = "https://files.pythonhosted.org/packages/11/dd/5820e09213a3f7c0ee5aff20fce8b362ce935f9dd9958827274de4eaeec6/bitsandbytes-0.49.1-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:acd4730a0db3762d286707f4a3bc1d013d21dd5f0e441900da57ec4198578d4e", size = 31065659, upload-time = "2026-01-08T14:31:28.676Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/4f/02d3cb62a1b0b5a1ca7ff03dce3606be1bf3ead4744f47eb762dbf471069/bitsandbytes-0.49.1-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:e7940bf32457dc2e553685285b2a86e82f5ec10b2ae39776c408714f9ae6983c", size = 59054193, upload-time = "2026-01-08T14:31:31.743Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/53/7cfbe3a93354764be85c2dfcbfc5b6536413e598155aa7ef7e85d74c9e49/bitsandbytes-0.49.1-py3-none-win_amd64.whl", hash = "sha256:6ead0763f4beb936f9a09acb49ec094a259180906fc0605d9ca0617249c3c798", size = 54700630, upload-time = "2026-01-08T14:31:35.638Z" },
+]
+
+[[package]]
+name = "blis"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d0/d0/d8cc8c9a4488a787e7fa430f6055e5bd1ddb22c340a751d9e901b82e2efe/blis-1.3.3.tar.gz", hash = "sha256:034d4560ff3cc43e8aa37e188451b0440e3261d989bb8a42ceee865607715ecd", size = 2644873, upload-time = "2025-11-17T12:28:30.511Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/16/d1/429cf0cf693d4c7dc2efed969bd474e315aab636e4a95f66c4ed7264912d/blis-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a1c74e100665f8e918ebdbae2794576adf1f691680b5cdb8b29578432f623ef", size = 6929663, upload-time = "2025-11-17T12:27:44.482Z" },
+ { url = "https://files.pythonhosted.org/packages/11/69/363c8df8d98b3cc97be19aad6aabb2c9c53f372490d79316bdee92d476e7/blis-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f6c595185176ce021316263e1a1d636a3425b6c48366c1fd712d08d0b71849a", size = 1230939, upload-time = "2025-11-17T12:27:46.19Z" },
+ { url = "https://files.pythonhosted.org/packages/96/2a/fbf65d906d823d839076c5150a6f8eb5ecbc5f9135e0b6510609bda1e6b7/blis-1.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d734b19fba0be7944f272dfa7b443b37c61f9476d9ab054a9ac53555ceadd2e0", size = 2818835, upload-time = "2025-11-17T12:27:48.167Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ad/58deaa3ad856dd3cc96493e40ffd2ed043d18d4d304f85a65cde1ccbf644/blis-1.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ef6d6e2b599a3a2788eb6d9b443533961265aa4ec49d574ed4bb846e548dcdb", size = 11366550, upload-time = "2025-11-17T12:27:49.958Z" },
+ { url = "https://files.pythonhosted.org/packages/78/82/816a7adfe1f7acc8151f01ec86ef64467a3c833932d8f19f8e06613b8a4e/blis-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8c888438ae99c500422d50698e3028b65caa8ebb44e24204d87fda2df64058f7", size = 3023686, upload-time = "2025-11-17T12:27:52.062Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/e2/0e93b865f648b5519360846669a35f28ee8f4e1d93d054f6850d8afbabde/blis-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8177879fd3590b5eecdd377f9deafb5dc8af6d684f065bd01553302fb3fcf9a7", size = 14250939, upload-time = "2025-11-17T12:27:53.847Z" },
+ { url = "https://files.pythonhosted.org/packages/20/07/fb43edc2ff0a6a367e4a94fc39eb3b85aa1e55e24cc857af2db145ce9f0d/blis-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f20f7ad69aaffd1ce14fe77de557b6df9b61e0c9e582f75a843715d836b5c8af", size = 6192759, upload-time = "2025-11-17T12:27:56.176Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/f7/d26e62d9be3d70473a63e0a5d30bae49c2fe138bebac224adddcdef8a7ce/blis-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1e647341f958421a86b028a2efe16ce19c67dba2a05f79e8f7e80b1ff45328aa", size = 6928322, upload-time = "2025-11-17T12:27:57.965Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/78/750d12da388f714958eb2f2fd177652323bbe7ec528365c37129edd6eb84/blis-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d563160f874abb78a57e346f07312c5323f7ad67b6370052b6b17087ef234a8e", size = 1229635, upload-time = "2025-11-17T12:28:00.118Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/36/eac4199c5b200a5f3e93cad197da8d26d909f218eb444c4f552647c95240/blis-1.3.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:30b8a5b90cb6cb81d1ada9ae05aa55fb8e70d9a0ae9db40d2401bb9c1c8f14c4", size = 2815650, upload-time = "2025-11-17T12:28:02.544Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/51/472e7b36a6bedb5242a9757e7486f702c3619eff76e256735d0c8b1679c6/blis-1.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9f5c53b277f6ac5b3ca30bc12ebab7ea16c8f8c36b14428abb56924213dc127", size = 11359008, upload-time = "2025-11-17T12:28:04.589Z" },
+ { url = "https://files.pythonhosted.org/packages/84/da/d0dfb6d6e6321ae44df0321384c32c322bd07b15740d7422727a1a49fc5d/blis-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6297e7616c158b305c9a8a4e47ca5fc9b0785194dd96c903b1a1591a7ca21ddf", size = 3011959, upload-time = "2025-11-17T12:28:06.862Z" },
+ { url = "https://files.pythonhosted.org/packages/20/c5/2b0b5e556fa0364ed671051ea078a6d6d7b979b1cfef78d64ad3ca5f0c7f/blis-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3f966ca74f89f8a33e568b9a1d71992fc9a0d29a423e047f0a212643e21b5458", size = 14232456, upload-time = "2025-11-17T12:28:08.779Z" },
+ { url = "https://files.pythonhosted.org/packages/31/07/4cdc81a47bf862c0b06d91f1bc6782064e8b69ac9b5d4ff51d97e4ff03da/blis-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:7a0fc4b237a3a453bdc3c7ab48d91439fcd2d013b665c46948d9eaf9c3e45a97", size = 6192624, upload-time = "2025-11-17T12:28:14.197Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/8a/80f7c68fbc24a76fc9c18522c46d6d69329c320abb18e26a707a5d874083/blis-1.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c3e33cfbf22a418373766816343fcfcd0556012aa3ffdf562c29cddec448a415", size = 6934081, upload-time = "2025-11-17T12:28:16.436Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/52/d1aa3a51a7fc299b0c89dcaa971922714f50b1202769eebbdaadd1b5cff7/blis-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6f165930e8d3a85c606d2003211497e28d528c7416fbfeafb6b15600963f7c9b", size = 1231486, upload-time = "2025-11-17T12:28:18.008Z" },
+ { url = "https://files.pythonhosted.org/packages/99/4f/badc7bd7f74861b26c10123bba7b9d16f99cd9535ad0128780360713820f/blis-1.3.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:878d4d96d8f2c7a2459024f013f2e4e5f46d708b23437dae970d998e7bff14a0", size = 2814944, upload-time = "2025-11-17T12:28:19.654Z" },
+ { url = "https://files.pythonhosted.org/packages/72/a6/f62a3bd814ca19ec7e29ac889fd354adea1217df3183e10217de51e2eb8b/blis-1.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f36c0ca84a05ee5d3dbaa38056c4423c1fc29948b17a7923dd2fed8967375d74", size = 11345825, upload-time = "2025-11-17T12:28:21.354Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/6c/671af79ee42bc4c968cae35c091ac89e8721c795bfa4639100670dc59139/blis-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e5a662c48cd4aad5dae1a950345df23957524f071315837a4c6feb7d3b288990", size = 3008771, upload-time = "2025-11-17T12:28:23.637Z" },
+ { url = "https://files.pythonhosted.org/packages/be/92/7cd7f8490da7c98ee01557f2105885cc597217b0e7fd2eeb9e22cdd4ef23/blis-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9de26fbd72bac900c273b76d46f0b45b77a28eace2e01f6ac6c2239531a413bb", size = 14219213, upload-time = "2025-11-17T12:28:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/de/acae8e9f9a1f4bb393d41c8265898b0f29772e38eac14e9f69d191e2c006/blis-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:9e5fdf4211b1972400f8ff6dafe87cb689c5d84f046b4a76b207c0bd2270faaf", size = 6324695, upload-time = "2025-11-17T12:28:28.401Z" },
+]
+
+[[package]]
+name = "catalogue"
+version = "2.0.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/38/b4/244d58127e1cdf04cf2dc7d9566f0d24ef01d5ce21811bab088ecc62b5ea/catalogue-2.0.10.tar.gz", hash = "sha256:4f56daa940913d3f09d589c191c74e5a6d51762b3a9e37dd53b7437afd6cda15", size = 19561, upload-time = "2023-09-25T06:29:24.962Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/96/d32b941a501ab566a16358d68b6eb4e4acc373fab3c3c4d7d9e649f7b4bb/catalogue-2.0.10-py3-none-any.whl", hash = "sha256:58c2de0020aa90f4a2da7dfad161bf7b3b054c86a5f09fcedc0b2b740c109a9f", size = 17325, upload-time = "2023-09-25T06:29:23.337Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
+ { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
+ { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
+ { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
+ { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
+ { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
+ { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
+ { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
+ { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
+ { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "cloudpathlib"
+version = "0.23.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f4/18/2ac35d6b3015a0c74e923d94fc69baf8307f7c3233de015d69f99e17afa8/cloudpathlib-0.23.0.tar.gz", hash = "sha256:eb38a34c6b8a048ecfd2b2f60917f7cbad4a105b7c979196450c2f541f4d6b4b", size = 53126, upload-time = "2025-10-07T22:47:56.278Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ae/8a/c4bb04426d608be4a3171efa2e233d2c59a5c8937850c10d098e126df18e/cloudpathlib-0.23.0-py3-none-any.whl", hash = "sha256:8520b3b01468fee77de37ab5d50b1b524ea6b4a8731c35d1b7407ac0cd716002", size = 62755, upload-time = "2025-10-07T22:47:54.905Z" },
+]
+
+[[package]]
+name = "codegamer"
+version = "0.1.0"
+source = { virtual = "." }
+dependencies = [
+ { name = "hello-agents", extra = ["all"] },
+ { name = "openai" },
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "textual" },
+ { name = "tiktoken" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "hello-agents", extras = ["all"], specifier = "==0.2.7" },
+ { name = "openai", specifier = ">=1.0.0" },
+ { name = "pydantic", specifier = ">=2.0.0" },
+ { name = "python-dotenv", specifier = ">=1.0.0" },
+ { name = "textual", specifier = ">=7.4.0" },
+ { name = "tiktoken", specifier = ">=0.5.0" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coloredlogs"
+version = "15.0.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "humanfriendly" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
+]
+
+[[package]]
+name = "confection"
+version = "0.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "srsly" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/51/d3/57c6631159a1b48d273b40865c315cf51f89df7a9d1101094ef12e3a37c2/confection-0.1.5.tar.gz", hash = "sha256:8e72dd3ca6bd4f48913cd220f10b8275978e740411654b6e8ca6d7008c590f0e", size = 38924, upload-time = "2024-05-31T16:17:01.559Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/00/3106b1854b45bd0474ced037dfe6b73b90fe68a68968cef47c23de3d43d2/confection-0.1.5-py3-none-any.whl", hash = "sha256:e29d3c3f8eac06b3f77eb9dfb4bf2fc6bcc9622a98ca00a698e3d019c6430b14", size = 35451, upload-time = "2024-05-31T16:16:59.075Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
+ { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
+ { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
+ { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
+ { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
+ { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
+ { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
+ { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
+ { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
+ { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
+ { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
+ { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
+ { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
+ { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
+]
+
+[[package]]
+name = "cryptography"
+version = "46.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" },
+ { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" },
+ { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" },
+ { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" },
+ { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" },
+ { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" },
+ { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" },
+ { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" },
+ { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" },
+ { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" },
+ { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" },
+ { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" },
+ { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" },
+ { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" },
+ { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
+ { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
+]
+
+[[package]]
+name = "cuda-bindings"
+version = "12.9.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cuda-pathfinder", marker = "sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" },
+ { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" },
+]
+
+[[package]]
+name = "cuda-pathfinder"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/02/4dbe7568a42e46582248942f54dc64ad094769532adbe21e525e4edf7bc4/cuda_pathfinder-1.3.3-py3-none-any.whl", hash = "sha256:9984b664e404f7c134954a771be8775dfd6180ea1e1aef4a5a37d4be05d9bbb1", size = 27154, upload-time = "2025-12-04T22:35:08.996Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "cyclopts"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "docstring-parser" },
+ { name = "rich" },
+ { name = "rich-rst" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d4/93/6085aa89c3fff78a5180987354538d72e43b0db27e66a959302d0c07821a/cyclopts-4.5.1.tar.gz", hash = "sha256:fadc45304763fd9f5d6033727f176898d17a1778e194436964661a005078a3dd", size = 162075, upload-time = "2026-01-25T15:23:54.07Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1c/7c/996760c30f1302704af57c66ff2d723f7d656d0d0b93563b5528a51484bb/cyclopts-4.5.1-py3-none-any.whl", hash = "sha256:0642c93601e554ca6b7b9abd81093847ea4448b2616280f2a0952416574e8c7a", size = 199807, upload-time = "2026-01-25T15:23:55.219Z" },
+]
+
+[[package]]
+name = "cymem"
+version = "2.0.13"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/2f0fbb32535c3731b7c2974c569fb9325e0a38ed5565a08e1139a3b71e82/cymem-2.0.13.tar.gz", hash = "sha256:1c91a92ae8c7104275ac26bd4d29b08ccd3e7faff5893d3858cb6fadf1bc1588", size = 12320, upload-time = "2025-11-14T14:58:36.902Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/52/478a2911ab5028cb710b4900d64aceba6f4f882fcb13fd8d40a456a1b6dc/cymem-2.0.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8afbc5162a0fe14b6463e1c4e45248a1b2fe2cbcecc8a5b9e511117080da0eb", size = 43745, upload-time = "2025-11-14T14:57:32.52Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/71/f0f8adee945524774b16af326bd314a14a478ed369a728a22834e6785a18/cymem-2.0.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9251d889348fe79a75e9b3e4d1b5fa651fca8a64500820685d73a3acc21b6a8", size = 42927, upload-time = "2025-11-14T14:57:33.827Z" },
+ { url = "https://files.pythonhosted.org/packages/62/6d/159780fe162ff715d62b809246e5fc20901cef87ca28b67d255a8d741861/cymem-2.0.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:742fc19764467a49ed22e56a4d2134c262d73a6c635409584ae3bf9afa092c33", size = 258346, upload-time = "2025-11-14T14:57:34.917Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/12/678d16f7aa1996f947bf17b8cfb917ea9c9674ef5e2bd3690c04123d5680/cymem-2.0.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f190a92fe46197ee64d32560eb121c2809bb843341733227f51538ce77b3410d", size = 260843, upload-time = "2025-11-14T14:57:36.503Z" },
+ { url = "https://files.pythonhosted.org/packages/31/5d/0dd8c167c08cd85e70d274b7235cfe1e31b3cebc99221178eaf4bbb95c6f/cymem-2.0.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d670329ee8dbbbf241b7c08069fe3f1d3a1a3e2d69c7d05ea008a7010d826298", size = 254607, upload-time = "2025-11-14T14:57:38.036Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/c9/d6514a412a1160aa65db539836b3d47f9b59f6675f294ec34ae32f867c82/cymem-2.0.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a84ba3178d9128b9ffb52ce81ebab456e9fe959125b51109f5b73ebdfc6b60d6", size = 262421, upload-time = "2025-11-14T14:57:39.265Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/fe/3ee37d02ca4040f2fb22d34eb415198f955862b5dd47eee01df4c8f5454c/cymem-2.0.13-cp312-cp312-win_amd64.whl", hash = "sha256:2ff1c41fd59b789579fdace78aa587c5fc091991fa59458c382b116fc36e30dc", size = 40176, upload-time = "2025-11-14T14:57:40.706Z" },
+ { url = "https://files.pythonhosted.org/packages/94/fb/1b681635bfd5f2274d0caa8f934b58435db6c091b97f5593738065ddb786/cymem-2.0.13-cp312-cp312-win_arm64.whl", hash = "sha256:6bbd701338df7bf408648191dff52472a9b334f71bcd31a21a41d83821050f67", size = 35959, upload-time = "2025-11-14T14:57:41.682Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/0f/95a4d1e3bebfdfa7829252369357cf9a764f67569328cd9221f21e2c952e/cymem-2.0.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:891fd9030293a8b652dc7fb9fdc79a910a6c76fc679cd775e6741b819ffea476", size = 43478, upload-time = "2025-11-14T14:57:42.682Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/a0/8fc929cc29ae466b7b4efc23ece99cbd3ea34992ccff319089c624d667fd/cymem-2.0.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:89c4889bd16513ce1644ccfe1e7c473ba7ca150f0621e66feac3a571bde09e7e", size = 42695, upload-time = "2025-11-14T14:57:43.741Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b3/deeb01354ebaf384438083ffe0310209ef903db3e7ba5a8f584b06d28387/cymem-2.0.13-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:45dcaba0f48bef9cc3d8b0b92058640244a95a9f12542210b51318da97c2cf28", size = 250573, upload-time = "2025-11-14T14:57:44.81Z" },
+ { url = "https://files.pythonhosted.org/packages/36/36/bc980b9a14409f3356309c45a8d88d58797d02002a9d794dd6c84e809d3a/cymem-2.0.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e96848faaafccc0abd631f1c5fb194eac0caee4f5a8777fdbb3e349d3a21741c", size = 254572, upload-time = "2025-11-14T14:57:46.023Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/dd/a12522952624685bd0f8968e26d2ed6d059c967413ce6eb52292f538f1b0/cymem-2.0.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e02d3e2c3bfeb21185d5a4a70790d9df40629a87d8d7617dc22b4e864f665fa3", size = 248060, upload-time = "2025-11-14T14:57:47.605Z" },
+ { url = "https://files.pythonhosted.org/packages/08/11/5dc933ddfeb2dfea747a0b935cb965b9a7580b324d96fc5f5a1b5ff8df29/cymem-2.0.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fece5229fd5ecdcd7a0738affb8c59890e13073ae5626544e13825f26c019d3c", size = 254601, upload-time = "2025-11-14T14:57:48.861Z" },
+ { url = "https://files.pythonhosted.org/packages/70/66/d23b06166864fa94e13a98e5922986ce774832936473578febce64448d75/cymem-2.0.13-cp313-cp313-win_amd64.whl", hash = "sha256:38aefeb269597c1a0c2ddf1567dd8605489b661fa0369c6406c1acd433b4c7ba", size = 40103, upload-time = "2025-11-14T14:57:50.396Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/9e/c7b21271ab88a21760f3afdec84d2bc09ffa9e6c8d774ad9d4f1afab0416/cymem-2.0.13-cp313-cp313-win_arm64.whl", hash = "sha256:717270dcfd8c8096b479c42708b151002ff98e434a7b6f1f916387a6c791e2ad", size = 36016, upload-time = "2025-11-14T14:57:51.611Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/28/d3b03427edc04ae04910edf1c24b993881c3ba93a9729a42bcbb816a1808/cymem-2.0.13-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7e1a863a7f144ffb345397813701509cfc74fc9ed360a4d92799805b4b865dd1", size = 46429, upload-time = "2025-11-14T14:57:52.582Z" },
+ { url = "https://files.pythonhosted.org/packages/35/a9/7ed53e481f47ebfb922b0b42e980cec83e98ccb2137dc597ea156642440c/cymem-2.0.13-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c16cb80efc017b054f78998c6b4b013cef509c7b3d802707ce1f85a1d68361bf", size = 46205, upload-time = "2025-11-14T14:57:53.64Z" },
+ { url = "https://files.pythonhosted.org/packages/61/39/a3d6ad073cf7f0fbbb8bbf09698c3c8fac11be3f791d710239a4e8dd3438/cymem-2.0.13-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0d78a27c88b26c89bd1ece247d1d5939dba05a1dae6305aad8fd8056b17ddb51", size = 296083, upload-time = "2025-11-14T14:57:55.922Z" },
+ { url = "https://files.pythonhosted.org/packages/36/0c/20697c8bc19f624a595833e566f37d7bcb9167b0ce69de896eba7cfc9c2d/cymem-2.0.13-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6d36710760f817194dacb09d9fc45cb6a5062ed75e85f0ef7ad7aeeb13d80cc3", size = 286159, upload-time = "2025-11-14T14:57:57.106Z" },
+ { url = "https://files.pythonhosted.org/packages/82/d4/9326e3422d1c2d2b4a8fb859bdcce80138f6ab721ddafa4cba328a505c71/cymem-2.0.13-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c8f30971cadd5dcf73bcfbbc5849b1f1e1f40db8cd846c4aa7d3b5e035c7b583", size = 288186, upload-time = "2025-11-14T14:57:58.334Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/bc/68da7dd749b72884dc22e898562f335002d70306069d496376e5ff3b6153/cymem-2.0.13-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9d441d0e45798ec1fd330373bf7ffa6b795f229275f64016b6a193e6e2a51522", size = 290353, upload-time = "2025-11-14T14:58:00.562Z" },
+ { url = "https://files.pythonhosted.org/packages/50/23/dbf2ad6ecd19b99b3aab6203b1a06608bbd04a09c522d836b854f2f30f73/cymem-2.0.13-cp313-cp313t-win_amd64.whl", hash = "sha256:d1c950eebb9f0f15e3ef3591313482a5a611d16fc12d545e2018cd607f40f472", size = 44764, upload-time = "2025-11-14T14:58:01.793Z" },
+ { url = "https://files.pythonhosted.org/packages/54/3f/35701c13e1fc7b0895198c8b20068c569a841e0daf8e0b14d1dc0816b28f/cymem-2.0.13-cp313-cp313t-win_arm64.whl", hash = "sha256:042e8611ef862c34a97b13241f5d0da86d58aca3cecc45c533496678e75c5a1f", size = 38964, upload-time = "2025-11-14T14:58:02.87Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/2e/f0e1596010a9a57fa9ebd124a678c07c5b2092283781ae51e79edcf5cb98/cymem-2.0.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d2a4bf67db76c7b6afc33de44fb1c318207c3224a30da02c70901936b5aafdf1", size = 43812, upload-time = "2025-11-14T14:58:04.227Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/45/8ccc21df08fcbfa6aa3efeb7efc11a1c81c90e7476e255768bb9c29ba02a/cymem-2.0.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:92a2ce50afa5625fb5ce7c9302cee61e23a57ccac52cd0410b4858e572f8614b", size = 42951, upload-time = "2025-11-14T14:58:05.424Z" },
+ { url = "https://files.pythonhosted.org/packages/01/8c/fe16531631f051d3d1226fa42e2d76fd2c8d5cfa893ec93baee90c7a9d90/cymem-2.0.13-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bc116a70cc3a5dc3d1684db5268eff9399a0be8603980005e5b889564f1ea42f", size = 249878, upload-time = "2025-11-14T14:58:06.95Z" },
+ { url = "https://files.pythonhosted.org/packages/47/4b/39d67b80ffb260457c05fcc545de37d82e9e2dbafc93dd6b64f17e09b933/cymem-2.0.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:68489bf0035c4c280614067ab6a82815b01dc9fcd486742a5306fe9f68deb7ef", size = 252571, upload-time = "2025-11-14T14:58:08.232Z" },
+ { url = "https://files.pythonhosted.org/packages/53/0e/76f6531f74dfdfe7107899cce93ab063bb7ee086ccd3910522b31f623c08/cymem-2.0.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:03cb7bdb55718d5eb6ef0340b1d2430ba1386db30d33e9134d01ba9d6d34d705", size = 248555, upload-time = "2025-11-14T14:58:09.429Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/7c/eee56757db81f0aefc2615267677ae145aff74228f529838425057003c0d/cymem-2.0.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1710390e7fb2510a8091a1991024d8ae838fd06b02cdfdcd35f006192e3c6b0e", size = 254177, upload-time = "2025-11-14T14:58:10.594Z" },
+ { url = "https://files.pythonhosted.org/packages/77/e0/a4b58ec9e53c836dce07ef39837a64a599f4a21a134fc7ca57a3a8f9a4b5/cymem-2.0.13-cp314-cp314-win_amd64.whl", hash = "sha256:ac699c8ec72a3a9de8109bd78821ab22f60b14cf2abccd970b5ff310e14158ed", size = 40853, upload-time = "2025-11-14T14:58:12.116Z" },
+ { url = "https://files.pythonhosted.org/packages/61/81/9931d1f83e5aeba175440af0b28f0c2e6f71274a5a7b688bc3e907669388/cymem-2.0.13-cp314-cp314-win_arm64.whl", hash = "sha256:90c2d0c04bcda12cd5cebe9be93ce3af6742ad8da96e1b1907e3f8e00291def1", size = 36970, upload-time = "2025-11-14T14:58:13.114Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/ef/af447c2184dec6dec973be14614df8ccb4d16d1c74e0784ab4f02538433c/cymem-2.0.13-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff036bbc1464993552fd1251b0a83fe102af334b301e3896d7aa05a4999ad042", size = 46804, upload-time = "2025-11-14T14:58:14.113Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/95/e10f33a8d4fc17f9b933d451038218437f9326c2abb15a3e7f58ce2a06ec/cymem-2.0.13-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fb8291691ba7ff4e6e000224cc97a744a8d9588418535c9454fd8436911df612", size = 46254, upload-time = "2025-11-14T14:58:15.156Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/7a/5efeb2d2ea6ebad2745301ad33a4fa9a8f9a33b66623ee4d9185683007a6/cymem-2.0.13-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d8d06ea59006b1251ad5794bcc00121e148434826090ead0073c7b7fedebe431", size = 296061, upload-time = "2025-11-14T14:58:16.254Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/28/2a3f65842cc8443c2c0650cf23d525be06c8761ab212e0a095a88627be1b/cymem-2.0.13-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c0046a619ecc845ccb4528b37b63426a0cbcb4f14d7940add3391f59f13701e6", size = 285784, upload-time = "2025-11-14T14:58:17.412Z" },
+ { url = "https://files.pythonhosted.org/packages/98/73/dd5f9729398f0108c2e71d942253d0d484d299d08b02e474d7cfc43ed0b0/cymem-2.0.13-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:18ad5b116a82fa3674bc8838bd3792891b428971e2123ae8c0fd3ca472157c5e", size = 288062, upload-time = "2025-11-14T14:58:20.225Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/01/ffe51729a8f961a437920560659073e47f575d4627445216c1177ecd4a41/cymem-2.0.13-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:666ce6146bc61b9318aa70d91ce33f126b6344a25cf0b925621baed0c161e9cc", size = 290465, upload-time = "2025-11-14T14:58:21.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ac/c9e7d68607f71ef978c81e334ab2898b426944c71950212b1467186f69f9/cymem-2.0.13-cp314-cp314t-win_amd64.whl", hash = "sha256:84c1168c563d9d1e04546cb65e3e54fde2bf814f7c7faf11fc06436598e386d1", size = 46665, upload-time = "2025-11-14T14:58:23.512Z" },
+ { url = "https://files.pythonhosted.org/packages/66/66/150e406a2db5535533aa3c946de58f0371f2e412e23f050c704588023e6e/cymem-2.0.13-cp314-cp314t-win_arm64.whl", hash = "sha256:e9027764dc5f1999fb4b4cabee1d0322c59e330c0a6485b436a68275f614277f", size = 39715, upload-time = "2025-11-14T14:58:24.773Z" },
+]
+
+[[package]]
+name = "datasets"
+version = "4.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dill" },
+ { name = "filelock" },
+ { name = "fsspec", extra = ["http"] },
+ { name = "httpx" },
+ { name = "huggingface-hub" },
+ { name = "multiprocess" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pandas" },
+ { name = "pyarrow" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "tqdm" },
+ { name = "xxhash" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/bf/bb927bde63d649296c83e883171ae77074717c1b80fe2868b328bd0dbcbb/datasets-4.5.0.tar.gz", hash = "sha256:00c698ce1c2452e646cc5fad47fef39d3fe78dd650a8a6eb205bb45eb63cd500", size = 588384, upload-time = "2026-01-14T18:27:54.297Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/d5/0d563ea3c205eee226dc8053cf7682a8ac588db8acecd0eda2b587987a0b/datasets-4.5.0-py3-none-any.whl", hash = "sha256:b5d7e08096ffa407dd69e58b1c0271c9b2506140839b8d99af07375ad31b6726", size = 515196, upload-time = "2026-01-14T18:27:52.419Z" },
+]
+
+[[package]]
+name = "defusedxml"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
+]
+
+[[package]]
+name = "dill"
+version = "0.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/12/80/630b4b88364e9a8c8c5797f4602d0f76ef820909ee32f0bacb9f90654042/dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0", size = 186976, upload-time = "2025-04-16T00:41:48.867Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/3d/9373ad9c56321fdab5b41197068e1d8c25883b3fea29dd361f9b55116869/dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049", size = 119668, upload-time = "2025-04-16T00:41:47.671Z" },
+]
+
+[[package]]
+name = "distro"
+version = "1.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" },
+]
+
+[[package]]
+name = "dnspython"
+version = "2.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
+]
+
+[[package]]
+name = "docstring-parser"
+version = "0.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" },
+]
+
+[[package]]
+name = "docutils"
+version = "0.22.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
+]
+
+[[package]]
+name = "email-validator"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dnspython" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
+]
+
+[[package]]
+name = "evaluate"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "datasets" },
+ { name = "dill" },
+ { name = "fsspec", extra = ["http"] },
+ { name = "huggingface-hub" },
+ { name = "multiprocess" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pandas" },
+ { name = "requests" },
+ { name = "tqdm" },
+ { name = "xxhash" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ad/d0/0c17a8e6e8dc7245f22dea860557c32bae50fc4d287ae030cb0e8ab8720f/evaluate-0.4.6.tar.gz", hash = "sha256:e07036ca12b3c24331f83ab787f21cc2dbf3631813a1631e63e40897c69a3f21", size = 65716, upload-time = "2025-09-18T13:06:30.581Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/af/3e990d8d4002bbc9342adb4facd59506e653da93b2417de0fa6027cb86b1/evaluate-0.4.6-py3-none-any.whl", hash = "sha256:bca85bc294f338377b7ac2f861e21c308b11b2a285f510d7d5394d5df437db29", size = 84069, upload-time = "2025-09-18T13:06:29.265Z" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.128.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "pydantic" },
+ { name = "starlette" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
+]
+
+[[package]]
+name = "fastmcp"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "authlib" },
+ { name = "cyclopts" },
+ { name = "exceptiongroup" },
+ { name = "httpx" },
+ { name = "mcp" },
+ { name = "openapi-core" },
+ { name = "openapi-pydantic" },
+ { name = "pydantic", extra = ["email"] },
+ { name = "pyperclip" },
+ { name = "python-dotenv" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/00/a6/e3b46cd3e228635e0064c2648788b6f66a53bf0d0ddbf5fb44cca951f908/fastmcp-2.12.5.tar.gz", hash = "sha256:2dfd02e255705a4afe43d26caddbc864563036e233dbc6870f389ee523b39a6a", size = 7190263, upload-time = "2025-10-17T13:24:58.896Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d8/c1/9fb98c9649e15ea8cc691b4b09558b61dafb3dc0345f7322f8c4a8991ade/fastmcp-2.12.5-py3-none-any.whl", hash = "sha256:b1e542f9b83dbae7cecfdc9c73b062f77074785abda9f2306799116121344133", size = 329099, upload-time = "2025-10-17T13:24:57.518Z" },
+]
+
+[[package]]
+name = "ffmpy"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/d2/1c4c582d71bcc65c76fa69fab85de6257d50fdf6fd4a2317c53917e9a581/ffmpy-1.0.0.tar.gz", hash = "sha256:b12932e95435c8820f1cd041024402765f821971e4bae753b327fc02a6e12f8b", size = 5101, upload-time = "2025-11-11T06:24:23.856Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/55/56/dd3669eccebb6d8ac81e624542ebd53fe6f08e1b8f2f8d50aeb7e3b83f99/ffmpy-1.0.0-py3-none-any.whl", hash = "sha256:5640e5f0fd03fb6236d0e119b16ccf6522db1c826fdf35dcb87087b60fd7504f", size = 5614, upload-time = "2025-11-11T06:24:22.818Z" },
+]
+
+[[package]]
+name = "filelock"
+version = "3.20.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
+]
+
+[[package]]
+name = "flatbuffers"
+version = "25.12.19"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.61.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/16/7decaa24a1bd3a70c607b2e29f0adc6159f36a7e40eaba59846414765fd4/fonttools-4.61.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f3cb4a569029b9f291f88aafc927dd53683757e640081ca8c412781ea144565e", size = 2851593, upload-time = "2025-12-12T17:30:04.225Z" },
+ { url = "https://files.pythonhosted.org/packages/94/98/3c4cb97c64713a8cf499b3245c3bf9a2b8fd16a3e375feff2aed78f96259/fonttools-4.61.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41a7170d042e8c0024703ed13b71893519a1a6d6e18e933e3ec7507a2c26a4b2", size = 2400231, upload-time = "2025-12-12T17:30:06.47Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/37/82dbef0f6342eb01f54bca073ac1498433d6ce71e50c3c3282b655733b31/fonttools-4.61.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10d88e55330e092940584774ee5e8a6971b01fc2f4d3466a1d6c158230880796", size = 4954103, upload-time = "2025-12-12T17:30:08.432Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/44/f3aeac0fa98e7ad527f479e161aca6c3a1e47bb6996b053d45226fe37bf2/fonttools-4.61.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15acc09befd16a0fb8a8f62bc147e1a82817542d72184acca9ce6e0aeda9fa6d", size = 5004295, upload-time = "2025-12-12T17:30:10.56Z" },
+ { url = "https://files.pythonhosted.org/packages/14/e8/7424ced75473983b964d09f6747fa09f054a6d656f60e9ac9324cf40c743/fonttools-4.61.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e6bcdf33aec38d16508ce61fd81838f24c83c90a1d1b8c68982857038673d6b8", size = 4944109, upload-time = "2025-12-12T17:30:12.874Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/8b/6391b257fa3d0b553d73e778f953a2f0154292a7a7a085e2374b111e5410/fonttools-4.61.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5fade934607a523614726119164ff621e8c30e8fa1ffffbbd358662056ba69f0", size = 5093598, upload-time = "2025-12-12T17:30:15.79Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/71/fd2ea96cdc512d92da5678a1c98c267ddd4d8c5130b76d0f7a80f9a9fde8/fonttools-4.61.1-cp312-cp312-win32.whl", hash = "sha256:75da8f28eff26defba42c52986de97b22106cb8f26515b7c22443ebc9c2d3261", size = 2269060, upload-time = "2025-12-12T17:30:18.058Z" },
+ { url = "https://files.pythonhosted.org/packages/80/3b/a3e81b71aed5a688e89dfe0e2694b26b78c7d7f39a5ffd8a7d75f54a12a8/fonttools-4.61.1-cp312-cp312-win_amd64.whl", hash = "sha256:497c31ce314219888c0e2fce5ad9178ca83fe5230b01a5006726cdf3ac9f24d9", size = 2319078, upload-time = "2025-12-12T17:30:22.862Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" },
+ { url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" },
+ { url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" },
+ { url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" },
+ { url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" },
+ { url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" },
+ { url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" },
+]
+
+[[package]]
+name = "frozenlist"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" },
+ { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" },
+ { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" },
+ { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" },
+ { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" },
+ { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" },
+ { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" },
+ { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" },
+ { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" },
+ { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" },
+ { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" },
+ { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" },
+ { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" },
+ { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" },
+ { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" },
+ { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" },
+ { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2025.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" },
+]
+
+[package.optional-dependencies]
+http = [
+ { name = "aiohttp" },
+]
+
+[[package]]
+name = "gitdb"
+version = "4.0.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "smmap" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" },
+]
+
+[[package]]
+name = "gitpython"
+version = "3.1.46"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "gitdb" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/b5/59d16470a1f0dfe8c793f9ef56fd3826093fc52b3bd96d6b9d6c26c7e27b/gitpython-3.1.46.tar.gz", hash = "sha256:400124c7d0ef4ea03f7310ac2fbf7151e09ff97f2a3288d64a440c584a29c37f", size = 215371, upload-time = "2026-01-01T15:37:32.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
+]
+
+[[package]]
+name = "google-api-core"
+version = "2.29.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "google-auth" },
+ { name = "googleapis-common-protos" },
+ { name = "proto-plus" },
+ { name = "protobuf" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828, upload-time = "2026-01-08T22:21:39.269Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906, upload-time = "2026-01-08T22:21:36.093Z" },
+]
+
+[[package]]
+name = "google-auth"
+version = "2.48.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+ { name = "pyasn1-modules" },
+ { name = "rsa" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0c/41/242044323fbd746615884b1c16639749e73665b718209946ebad7ba8a813/google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce", size = 326522, upload-time = "2026-01-26T19:22:47.157Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" },
+]
+
+[[package]]
+name = "google-search-results"
+version = "2.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/77/30/b3a6f6a2e00f8153549c2fa345c58ae1ce8e5f3153c2fe0484d444c3abcb/google_search_results-2.4.2.tar.gz", hash = "sha256:603a30ecae2af8e600b22635757a6df275dad4b934f975e67878ccd640b78245", size = 18818, upload-time = "2023-03-10T11:13:09.953Z" }
+
+[[package]]
+name = "googleapis-common-protos"
+version = "1.72.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" },
+]
+
+[[package]]
+name = "gradio"
+version = "4.44.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "aiofiles" },
+ { name = "anyio" },
+ { name = "fastapi" },
+ { name = "ffmpy" },
+ { name = "gradio-client" },
+ { name = "httpx" },
+ { name = "huggingface-hub" },
+ { name = "importlib-resources" },
+ { name = "jinja2" },
+ { name = "markupsafe" },
+ { name = "matplotlib" },
+ { name = "numpy" },
+ { name = "orjson" },
+ { name = "packaging" },
+ { name = "pandas" },
+ { name = "pillow" },
+ { name = "pydantic" },
+ { name = "pydub" },
+ { name = "python-multipart" },
+ { name = "pyyaml" },
+ { name = "ruff", marker = "sys_platform != 'emscripten'" },
+ { name = "semantic-version" },
+ { name = "tomlkit" },
+ { name = "typer", marker = "sys_platform != 'emscripten'" },
+ { name = "typing-extensions" },
+ { name = "urllib3" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/45/0c/8848a5f628b0de1b5b691f4f47de36a93e9d4f3d4157ac1dd8d2d57a5dec/gradio-4.44.1.tar.gz", hash = "sha256:a68a52498ac6b63f8864ef84bf7866a70e7d07ebe913edf921e1d2a3708ad5ae", size = 28309860, upload-time = "2024-09-30T17:52:37.306Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/6e/c0726e138f64cd98379a7bf95f4f3b15dd5a9f004b172540cee5653ec820/gradio-4.44.1-py3-none-any.whl", hash = "sha256:c908850c638e4a176b22f95a758ce6a63ffbc2a7a5a74b23186ceeeedc23f4d9", size = 18068711, upload-time = "2024-09-30T17:52:33.249Z" },
+]
+
+[[package]]
+name = "gradio-client"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "fsspec" },
+ { name = "httpx" },
+ { name = "huggingface-hub" },
+ { name = "packaging" },
+ { name = "typing-extensions" },
+ { name = "websockets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7b/86/2697930b274a5c8479a44dcb65c02d7e7445e5d88a72524a0a65593cbb58/gradio_client-1.3.0.tar.gz", hash = "sha256:d904afeae4f5682add0a6a263542c10e7669ff6c9de0a53a5c2fc9b719a24bb8", size = 316713, upload-time = "2024-08-08T10:30:23.749Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/fe/7e9cb4d0e6aa74268fa31089189e4855882a0f2a36c45d359336946d4ae1/gradio_client-1.3.0-py3-none-any.whl", hash = "sha256:20c40cb4d56e18de1a025ccf58079f08a304e4fb2dfbcf7c2352815b2cb31091", size = 318688, upload-time = "2024-08-08T10:30:21.659Z" },
+]
+
+[[package]]
+name = "grpcio"
+version = "1.76.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" },
+ { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" },
+ { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" },
+ { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" },
+ { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" },
+ { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" },
+ { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" },
+ { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" },
+ { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" },
+ { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "h2"
+version = "4.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "hpack" },
+ { name = "hyperframe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" },
+]
+
+[[package]]
+name = "hello-agents"
+version = "0.2.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "networkx" },
+ { name = "numpy" },
+ { name = "openai" },
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "requests" },
+ { name = "tiktoken" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e3/cb/507a31be2df9104173701eb849a0c692de0d19ae2ecf8f1fc3f393194507/hello_agents-0.2.7.tar.gz", hash = "sha256:2777e2916dbb6b2e48ddc811dc681263bea60a8bfbc9ccaac24176ee011aad26", size = 617495, upload-time = "2025-10-23T05:16:04.645Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/a0/d70b652b6af2a28495025fab9104da36c01abff8995c847d412e79f6e683/hello_agents-0.2.7-py3-none-any.whl", hash = "sha256:2c0b1c03893537de2424a87ab6a274f24ce0e195d9c9f547694b60d03ec6d9ff", size = 242693, upload-time = "2025-10-23T05:16:03.205Z" },
+]
+
+[package.optional-dependencies]
+all = [
+ { name = "a2a-sdk" },
+ { name = "accelerate" },
+ { name = "bitsandbytes" },
+ { name = "datasets" },
+ { name = "evaluate" },
+ { name = "fastmcp" },
+ { name = "google-search-results" },
+ { name = "gradio" },
+ { name = "huggingface-hub" },
+ { name = "markitdown" },
+ { name = "matplotlib" },
+ { name = "neo4j" },
+ { name = "pandas" },
+ { name = "pdfminer-six" },
+ { name = "peft" },
+ { name = "pypdf" },
+ { name = "qdrant-client" },
+ { name = "scikit-learn" },
+ { name = "seaborn" },
+ { name = "sentence-transformers" },
+ { name = "spacy" },
+ { name = "tavily-python" },
+ { name = "tensorboard" },
+ { name = "torch" },
+ { name = "tqdm" },
+ { name = "transformers" },
+ { name = "trl" },
+ { name = "wandb" },
+]
+
+[[package]]
+name = "hf-xet"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" },
+ { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" },
+ { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" },
+ { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" },
+ { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" },
+ { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" },
+]
+
+[[package]]
+name = "hpack"
+version = "4.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" },
+]
+
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
+]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "certifi" },
+ { name = "httpcore" },
+ { name = "idna" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
+]
+
+[package.optional-dependencies]
+http2 = [
+ { name = "h2" },
+]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
+]
+
+[[package]]
+name = "huggingface-hub"
+version = "0.36.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" },
+]
+
+[[package]]
+name = "humanfriendly"
+version = "10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyreadline3", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
+]
+
+[[package]]
+name = "hyperframe"
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+]
+
+[[package]]
+name = "importlib-resources"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
+]
+
+[[package]]
+name = "isodate"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" },
+]
+
+[[package]]
+name = "jinja2"
+version = "3.1.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
+]
+
+[[package]]
+name = "jiter"
+version = "0.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" },
+ { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" },
+ { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" },
+ { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" },
+ { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" },
+ { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" },
+ { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658, upload-time = "2025-11-09T20:47:44.424Z" },
+ { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605, upload-time = "2025-11-09T20:47:45.973Z" },
+ { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803, upload-time = "2025-11-09T20:47:47.535Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120, upload-time = "2025-11-09T20:47:49.284Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918, upload-time = "2025-11-09T20:47:50.807Z" },
+ { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008, upload-time = "2025-11-09T20:47:52.211Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785, upload-time = "2025-11-09T20:47:53.512Z" },
+ { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108, upload-time = "2025-11-09T20:47:54.893Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937, upload-time = "2025-11-09T20:47:56.253Z" },
+ { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853, upload-time = "2025-11-09T20:47:58.32Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699, upload-time = "2025-11-09T20:47:59.686Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258, upload-time = "2025-11-09T20:48:01.01Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503, upload-time = "2025-11-09T20:48:02.35Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965, upload-time = "2025-11-09T20:48:03.783Z" },
+ { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831, upload-time = "2025-11-09T20:48:05.55Z" },
+ { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272, upload-time = "2025-11-09T20:48:06.951Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604, upload-time = "2025-11-09T20:48:08.328Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628, upload-time = "2025-11-09T20:48:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478, upload-time = "2025-11-09T20:48:10.898Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706, upload-time = "2025-11-09T20:48:12.266Z" },
+ { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894, upload-time = "2025-11-09T20:48:13.673Z" },
+ { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714, upload-time = "2025-11-09T20:48:15.083Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989, upload-time = "2025-11-09T20:48:16.706Z" },
+ { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615, upload-time = "2025-11-09T20:48:18.614Z" },
+ { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745, upload-time = "2025-11-09T20:48:20.117Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502, upload-time = "2025-11-09T20:48:21.543Z" },
+ { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845, upload-time = "2025-11-09T20:48:22.964Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701, upload-time = "2025-11-09T20:48:24.483Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029, upload-time = "2025-11-09T20:48:25.749Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960, upload-time = "2025-11-09T20:48:27.415Z" },
+ { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529, upload-time = "2025-11-09T20:48:29.125Z" },
+ { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974, upload-time = "2025-11-09T20:48:30.87Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932, upload-time = "2025-11-09T20:48:32.658Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243, upload-time = "2025-11-09T20:48:34.093Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315, upload-time = "2025-11-09T20:48:35.507Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714, upload-time = "2025-11-09T20:48:40.014Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168, upload-time = "2025-11-09T20:48:41.462Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893, upload-time = "2025-11-09T20:48:42.921Z" },
+ { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828, upload-time = "2025-11-09T20:48:44.278Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009, upload-time = "2025-11-09T20:48:45.726Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110, upload-time = "2025-11-09T20:48:47.033Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223, upload-time = "2025-11-09T20:48:49.076Z" },
+ { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564, upload-time = "2025-11-09T20:48:50.376Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" },
+ { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" },
+]
+
+[[package]]
+name = "joblib"
+version = "1.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
+]
+
+[[package]]
+name = "jsonschema"
+version = "4.26.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "jsonschema-specifications" },
+ { name = "referencing" },
+ { name = "rpds-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
+]
+
+[[package]]
+name = "jsonschema-path"
+version = "0.3.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pathable" },
+ { name = "pyyaml" },
+ { name = "referencing" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" },
+]
+
+[[package]]
+name = "jsonschema-specifications"
+version = "2025.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "referencing" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
+ { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
+ { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
+ { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
+ { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
+ { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" },
+ { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" },
+ { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" },
+ { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" },
+ { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" },
+ { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" },
+ { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" },
+ { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" },
+ { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" },
+ { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
+ { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
+ { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
+ { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
+ { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
+ { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
+ { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
+ { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
+]
+
+[[package]]
+name = "lazy-object-proxy"
+version = "1.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" },
+ { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" },
+ { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" },
+ { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" },
+ { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" },
+ { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" },
+ { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" },
+ { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" },
+ { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" },
+ { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/3a/277857b51ae419a1574557c0b12e0d06bf327b758ba94cafc664cb1e2f66/lazy_object_proxy-1.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c9defba70ab943f1df98a656247966d7729da2fe9c2d5d85346464bf320820a3", size = 26582, upload-time = "2025-08-22T13:49:49.366Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/b6/c5e0fa43535bb9c87880e0ba037cdb1c50e01850b0831e80eb4f4762f270/lazy_object_proxy-1.12.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6763941dbf97eea6b90f5b06eb4da9418cc088fce0e3883f5816090f9afcde4a", size = 71059, upload-time = "2025-08-22T13:49:50.488Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/7dcad19c685963c652624702f1a968ff10220b16bfcc442257038216bf55/lazy_object_proxy-1.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fdc70d81235fc586b9e3d1aeef7d1553259b62ecaae9db2167a5d2550dcc391a", size = 71034, upload-time = "2025-08-22T13:49:54.224Z" },
+ { url = "https://files.pythonhosted.org/packages/12/ac/34cbfb433a10e28c7fd830f91c5a348462ba748413cbb950c7f259e67aa7/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0a83c6f7a6b2bfc11ef3ed67f8cbe99f8ff500b05655d8e7df9aab993a6abc95", size = 69529, upload-time = "2025-08-22T13:49:55.29Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/6a/11ad7e349307c3ca4c0175db7a77d60ce42a41c60bcb11800aabd6a8acb8/lazy_object_proxy-1.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:256262384ebd2a77b023ad02fbcc9326282bcfd16484d5531154b02bc304f4c5", size = 70391, upload-time = "2025-08-22T13:49:56.35Z" },
+ { url = "https://files.pythonhosted.org/packages/59/97/9b410ed8fbc6e79c1ee8b13f8777a80137d4bc189caf2c6202358e66192c/lazy_object_proxy-1.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:7601ec171c7e8584f8ff3f4e440aa2eebf93e854f04639263875b8c2971f819f", size = 26988, upload-time = "2025-08-22T13:49:57.302Z" },
+]
+
+[[package]]
+name = "linkify-it-py"
+version = "2.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "uc-micro-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" },
+]
+
+[[package]]
+name = "magika"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "numpy" },
+ { name = "onnxruntime" },
+ { name = "python-dotenv" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a3/f3/3d1dcdd7b9c41d589f5cff252d32ed91cdf86ba84391cfc81d9d8773571d/magika-0.6.3.tar.gz", hash = "sha256:7cc52aa7359af861957043e2bf7265ed4741067251c104532765cd668c0c0cb1", size = 3042784, upload-time = "2025-10-30T15:22:34.499Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/e4/35c323beb3280482c94299d61626116856ac2d4ec16ecef50afc4fdd4291/magika-0.6.3-py3-none-any.whl", hash = "sha256:eda443d08006ee495e02083b32e51b98cb3696ab595a7d13900d8e2ef506ec9d", size = 2969474, upload-time = "2025-10-30T15:22:25.298Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8f/132b0d7cd51c02c39fd52658a5896276c30c8cc2fd453270b19db8c40f7e/magika-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:86901e64b05dde5faff408c9b8245495b2e1fd4c226e3393d3d2a3fee65c504b", size = 13358841, upload-time = "2025-10-30T15:22:27.413Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/03/5ed859be502903a68b7b393b17ae0283bf34195cfcca79ce2dc25b9290e7/magika-0.6.3-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:3d9661eedbdf445ac9567e97e7ceefb93545d77a6a32858139ea966b5806fb64", size = 15367335, upload-time = "2025-10-30T15:22:29.907Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/9e/f8ee7d644affa3b80efdd623a3d75865c8f058f3950cb87fb0c48e3559bc/magika-0.6.3-py3-none-win_amd64.whl", hash = "sha256:e57f75674447b20cab4db928ae58ab264d7d8582b55183a0b876711c2b2787f3", size = 12692831, upload-time = "2025-10-30T15:22:32.063Z" },
+]
+
+[[package]]
+name = "markdown"
+version = "3.10.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/b1/af95bcae8549f1f3fd70faacb29075826a0d689a27f232e8cee315efa053/markdown-3.10.1.tar.gz", hash = "sha256:1c19c10bd5c14ac948c53d0d762a04e2fa35a6d58a6b7b1e6bfcbe6fefc0001a", size = 365402, upload-time = "2026-01-21T18:09:28.206Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/59/1b/6ef961f543593969d25b2afe57a3564200280528caa9bd1082eecdd7b3bc/markdown-3.10.1-py3-none-any.whl", hash = "sha256:867d788939fe33e4b736426f5b9f651ad0c0ae0ecf89df0ca5d1176c70812fe3", size = 107684, upload-time = "2026-01-21T18:09:27.203Z" },
+]
+
+[[package]]
+name = "markdown-it-py"
+version = "4.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mdurl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
+]
+
+[package.optional-dependencies]
+linkify = [
+ { name = "linkify-it-py" },
+]
+
+[[package]]
+name = "markdownify"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3f/bc/c8c8eea5335341306b0fa7e1cb33c5e1c8d24ef70ddd684da65f41c49c92/markdownify-1.2.2.tar.gz", hash = "sha256:b274f1b5943180b031b699b199cbaeb1e2ac938b75851849a31fd0c3d6603d09", size = 18816, upload-time = "2025-11-16T19:21:18.565Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/ce/f1e3e9d959db134cedf06825fae8d5b294bd368aacdd0831a3975b7c4d55/markdownify-1.2.2-py3-none-any.whl", hash = "sha256:3f02d3cc52714084d6e589f70397b6fc9f2f3a8531481bf35e8cc39f975e186a", size = 15724, upload-time = "2025-11-16T19:21:17.622Z" },
+]
+
+[[package]]
+name = "markitdown"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "beautifulsoup4" },
+ { name = "charset-normalizer" },
+ { name = "defusedxml" },
+ { name = "magika" },
+ { name = "markdownify" },
+ { name = "onnxruntime", marker = "sys_platform == 'win32'" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f3/4d/06567465c1886c2ea47bac24eab0c96bb6b4ecea47224323409dc9cbb614/markitdown-0.1.4.tar.gz", hash = "sha256:e72a481d1a50c82ff744e85e3289f79a940c5d0ad5ffa2b37c33de814c195bb1", size = 39951, upload-time = "2025-12-01T18:20:30.937Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/b3/6138d2b23d5b534b0fa3736987a2e11bcef5419cbc9286c8afd229d21558/markitdown-0.1.4-py3-none-any.whl", hash = "sha256:d7f3805716b22545f693d355e28e89584226c0614b3b80b7c4a3f825f068492d", size = 58314, upload-time = "2025-12-01T18:20:32.345Z" },
+]
+
+[[package]]
+name = "markupsafe"
+version = "2.1.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/87/5b/aae44c6655f3801e81aa3eef09dbbf012431987ba564d7231722f68df02d/MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b", size = 19384, upload-time = "2024-02-02T16:31:22.863Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/53/bd/583bf3e4c8d6a321938c13f49d44024dbe5ed63e0a7ba127e454a66da974/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1", size = 18215, upload-time = "2024-02-02T16:30:33.081Z" },
+ { url = "https://files.pythonhosted.org/packages/48/d6/e7cd795fc710292c3af3a06d80868ce4b02bfbbf370b7cee11d282815a2a/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4", size = 14069, upload-time = "2024-02-02T16:30:34.148Z" },
+ { url = "https://files.pythonhosted.org/packages/51/b5/5d8ec796e2a08fc814a2c7d2584b55f889a55cf17dd1a90f2beb70744e5c/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee", size = 29452, upload-time = "2024-02-02T16:30:35.149Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0d/2454f072fae3b5a137c119abf15465d1771319dfe9e4acbb31722a0fff91/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5", size = 28462, upload-time = "2024-02-02T16:30:36.166Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/75/fd6cb2e68780f72d47e6671840ca517bda5ef663d30ada7616b0462ad1e3/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b", size = 27869, upload-time = "2024-02-02T16:30:37.834Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/81/147c477391c2750e8fc7705829f7351cf1cd3be64406edcf900dc633feb2/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a", size = 33906, upload-time = "2024-02-02T16:30:39.366Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ff/9a52b71839d7a256b563e85d11050e307121000dcebc97df120176b3ad93/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f", size = 32296, upload-time = "2024-02-02T16:30:40.413Z" },
+ { url = "https://files.pythonhosted.org/packages/88/07/2dc76aa51b481eb96a4c3198894f38b480490e834479611a4053fbf08623/MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169", size = 33038, upload-time = "2024-02-02T16:30:42.243Z" },
+ { url = "https://files.pythonhosted.org/packages/96/0c/620c1fb3661858c0e37eb3cbffd8c6f732a67cd97296f725789679801b31/MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad", size = 16572, upload-time = "2024-02-02T16:30:43.326Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/14/c3554d512d5f9100a95e737502f4a2323a1959f6d0d01e0d0997b35f7b10/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb", size = 17127, upload-time = "2024-02-02T16:30:44.418Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.8"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8a/76/d3c6e3a13fe484ebe7718d14e269c9569c4eb0020a968a327acb3b9a8fe6/matplotlib-3.10.8.tar.gz", hash = "sha256:2299372c19d56bcd35cf05a2738308758d32b9eaed2371898d8f5bd33f084aa3", size = 34806269, upload-time = "2025-12-10T22:56:51.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/67/f997cdcbb514012eb0d10cd2b4b332667997fb5ebe26b8d41d04962fa0e6/matplotlib-3.10.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:64fcc24778ca0404ce0cb7b6b77ae1f4c7231cdd60e6778f999ee05cbd581b9a", size = 8260453, upload-time = "2025-12-10T22:55:30.709Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/65/07d5f5c7f7c994f12c768708bd2e17a4f01a2b0f44a1c9eccad872433e2e/matplotlib-3.10.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b9a5ca4ac220a0cdd1ba6bcba3608547117d30468fefce49bb26f55c1a3d5c58", size = 8148321, upload-time = "2025-12-10T22:55:33.265Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/f3/c5195b1ae57ef85339fd7285dfb603b22c8b4e79114bae5f4f0fcf688677/matplotlib-3.10.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3ab4aabc72de4ff77b3ec33a6d78a68227bf1123465887f9905ba79184a1cc04", size = 8716944, upload-time = "2025-12-10T22:55:34.922Z" },
+ { url = "https://files.pythonhosted.org/packages/00/f9/7638f5cc82ec8a7aa005de48622eecc3ed7c9854b96ba15bd76b7fd27574/matplotlib-3.10.8-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24d50994d8c5816ddc35411e50a86ab05f575e2530c02752e02538122613371f", size = 9550099, upload-time = "2025-12-10T22:55:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/57/61/78cd5920d35b29fd2a0fe894de8adf672ff52939d2e9b43cb83cd5ce1bc7/matplotlib-3.10.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:99eefd13c0dc3b3c1b4d561c1169e65fe47aab7b8158754d7c084088e2329466", size = 9613040, upload-time = "2025-12-10T22:55:38.715Z" },
+ { url = "https://files.pythonhosted.org/packages/30/4e/c10f171b6e2f44d9e3a2b96efa38b1677439d79c99357600a62cc1e9594e/matplotlib-3.10.8-cp312-cp312-win_amd64.whl", hash = "sha256:dd80ecb295460a5d9d260df63c43f4afbdd832d725a531f008dad1664f458adf", size = 8142717, upload-time = "2025-12-10T22:55:41.103Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/b9/15fd5541ef4f5b9a17eefd379356cf12175fe577424e7b1d80676516031a/matplotlib-3.10.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3f2e409836d7f5ac2f1c013110a4d50b9f7edc26328c108915f9075d7d7a91b6", size = 8261076, upload-time = "2025-12-10T22:55:44.648Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/a0/2ba3473c1b66b9c74dc7107c67e9008cb1782edbe896d4c899d39ae9cf78/matplotlib-3.10.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56271f3dac49a88d7fca5060f004d9d22b865f743a12a23b1e937a0be4818ee1", size = 8148794, upload-time = "2025-12-10T22:55:46.252Z" },
+ { url = "https://files.pythonhosted.org/packages/75/97/a471f1c3eb1fd6f6c24a31a5858f443891d5127e63a7788678d14e249aea/matplotlib-3.10.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0a7f52498f72f13d4a25ea70f35f4cb60642b466cbb0a9be951b5bc3f45a486", size = 8718474, upload-time = "2025-12-10T22:55:47.864Z" },
+ { url = "https://files.pythonhosted.org/packages/01/be/cd478f4b66f48256f42927d0acbcd63a26a893136456cd079c0cc24fbabf/matplotlib-3.10.8-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:646d95230efb9ca614a7a594d4fcacde0ac61d25e37dd51710b36477594963ce", size = 9549637, upload-time = "2025-12-10T22:55:50.048Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/7c/8dc289776eae5109e268c4fb92baf870678dc048a25d4ac903683b86d5bf/matplotlib-3.10.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f89c151aab2e2e23cb3fe0acad1e8b82841fd265379c4cecd0f3fcb34c15e0f6", size = 9613678, upload-time = "2025-12-10T22:55:52.21Z" },
+ { url = "https://files.pythonhosted.org/packages/64/40/37612487cc8a437d4dd261b32ca21fe2d79510fe74af74e1f42becb1bdb8/matplotlib-3.10.8-cp313-cp313-win_amd64.whl", hash = "sha256:e8ea3e2d4066083e264e75c829078f9e149fa119d27e19acd503de65e0b13149", size = 8142686, upload-time = "2025-12-10T22:55:54.253Z" },
+ { url = "https://files.pythonhosted.org/packages/66/52/8d8a8730e968185514680c2a6625943f70269509c3dcfc0dcf7d75928cb8/matplotlib-3.10.8-cp313-cp313-win_arm64.whl", hash = "sha256:c108a1d6fa78a50646029cb6d49808ff0fc1330fda87fa6f6250c6b5369b6645", size = 8012917, upload-time = "2025-12-10T22:55:56.268Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/27/51fe26e1062f298af5ef66343d8ef460e090a27fea73036c76c35821df04/matplotlib-3.10.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ad3d9833a64cf48cc4300f2b406c3d0f4f4724a91c0bd5640678a6ba7c102077", size = 8305679, upload-time = "2025-12-10T22:55:57.856Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/1e/4de865bc591ac8e3062e835f42dd7fe7a93168d519557837f0e37513f629/matplotlib-3.10.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:eb3823f11823deade26ce3b9f40dcb4a213da7a670013929f31d5f5ed1055b22", size = 8198336, upload-time = "2025-12-10T22:55:59.371Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/cb/2f7b6e75fb4dce87ef91f60cac4f6e34f4c145ab036a22318ec837971300/matplotlib-3.10.8-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9050fee89a89ed57b4fb2c1bfac9a3d0c57a0d55aed95949eedbc42070fea39", size = 8731653, upload-time = "2025-12-10T22:56:01.032Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b3/bd9c57d6ba670a37ab31fb87ec3e8691b947134b201f881665b28cc039ff/matplotlib-3.10.8-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b44d07310e404ba95f8c25aa5536f154c0a8ec473303535949e52eb71d0a1565", size = 9561356, upload-time = "2025-12-10T22:56:02.95Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/3d/8b94a481456dfc9dfe6e39e93b5ab376e50998cddfd23f4ae3b431708f16/matplotlib-3.10.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0a33deb84c15ede243aead39f77e990469fff93ad1521163305095b77b72ce4a", size = 9614000, upload-time = "2025-12-10T22:56:05.411Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cd/bc06149fe5585ba800b189a6a654a75f1f127e8aab02fd2be10df7fa500c/matplotlib-3.10.8-cp313-cp313t-win_amd64.whl", hash = "sha256:3a48a78d2786784cc2413e57397981fb45c79e968d99656706018d6e62e57958", size = 8220043, upload-time = "2025-12-10T22:56:07.551Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/de/b22cf255abec916562cc04eef457c13e58a1990048de0c0c3604d082355e/matplotlib-3.10.8-cp313-cp313t-win_arm64.whl", hash = "sha256:15d30132718972c2c074cd14638c7f4592bd98719e2308bccea40e0538bc0cb5", size = 8062075, upload-time = "2025-12-10T22:56:09.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/9c0ff7a2f11615e516c3b058e1e6e8f9614ddeca53faca06da267c48345d/matplotlib-3.10.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b53285e65d4fa4c86399979e956235deb900be5baa7fc1218ea67fbfaeaadd6f", size = 8262481, upload-time = "2025-12-10T22:56:10.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ca/e8ae28649fcdf039fda5ef554b40a95f50592a3c47e6f7270c9561c12b07/matplotlib-3.10.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32f8dce744be5569bebe789e46727946041199030db8aeb2954d26013a0eb26b", size = 8151473, upload-time = "2025-12-10T22:56:12.377Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/6f/009d129ae70b75e88cbe7e503a12a4c0670e08ed748a902c2568909e9eb5/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4cf267add95b1c88300d96ca837833d4112756045364f5c734a2276038dae27d", size = 9553896, upload-time = "2025-12-10T22:56:14.432Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/26/4221a741eb97967bc1fd5e4c52b9aa5a91b2f4ec05b59f6def4d820f9df9/matplotlib-3.10.8-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2cf5bd12cecf46908f286d7838b2abc6c91cda506c0445b8223a7c19a00df008", size = 9824193, upload-time = "2025-12-10T22:56:16.29Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/3abf75f38605772cf48a9daf5821cd4f563472f38b4b828c6fba6fa6d06e/matplotlib-3.10.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:41703cc95688f2516b480f7f339d8851a6035f18e100ee6a32bc0b8536a12a9c", size = 9615444, upload-time = "2025-12-10T22:56:18.155Z" },
+ { url = "https://files.pythonhosted.org/packages/93/a5/de89ac80f10b8dc615807ee1133cd99ac74082581196d4d9590bea10690d/matplotlib-3.10.8-cp314-cp314-win_amd64.whl", hash = "sha256:83d282364ea9f3e52363da262ce32a09dfe241e4080dcedda3c0db059d3c1f11", size = 8272719, upload-time = "2025-12-10T22:56:20.366Z" },
+ { url = "https://files.pythonhosted.org/packages/69/ce/b006495c19ccc0a137b48083168a37bd056392dee02f87dba0472f2797fe/matplotlib-3.10.8-cp314-cp314-win_arm64.whl", hash = "sha256:2c1998e92cd5999e295a731bcb2911c75f597d937341f3030cc24ef2733d78a8", size = 8144205, upload-time = "2025-12-10T22:56:22.239Z" },
+ { url = "https://files.pythonhosted.org/packages/68/d9/b31116a3a855bd313c6fcdb7226926d59b041f26061c6c5b1be66a08c826/matplotlib-3.10.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b5a2b97dbdc7d4f353ebf343744f1d1f1cca8aa8bfddb4262fcf4306c3761d50", size = 8305785, upload-time = "2025-12-10T22:56:24.218Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/90/6effe8103f0272685767ba5f094f453784057072f49b393e3ea178fe70a5/matplotlib-3.10.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3f5c3e4da343bba819f0234186b9004faba952cc420fbc522dc4e103c1985908", size = 8198361, upload-time = "2025-12-10T22:56:26.787Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/65/a73188711bea603615fc0baecca1061429ac16940e2385433cc778a9d8e7/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f62550b9a30afde8c1c3ae450e5eb547d579dd69b25c2fc7a1c67f934c1717a", size = 9561357, upload-time = "2025-12-10T22:56:28.953Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/3d/b5c5d5d5be8ce63292567f0e2c43dde9953d3ed86ac2de0a72e93c8f07a1/matplotlib-3.10.8-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:495672de149445ec1b772ff2c9ede9b769e3cb4f0d0aa7fa730d7f59e2d4e1c1", size = 9823610, upload-time = "2025-12-10T22:56:31.455Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/4b/e7beb6bbd49f6bae727a12b270a2654d13c397576d25bd6786e47033300f/matplotlib-3.10.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:595ba4d8fe983b88f0eec8c26a241e16d6376fe1979086232f481f8f3f67494c", size = 9614011, upload-time = "2025-12-10T22:56:33.85Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/e6/76f2813d31f032e65f6f797e3f2f6e4aab95b65015924b1c51370395c28a/matplotlib-3.10.8-cp314-cp314t-win_amd64.whl", hash = "sha256:25d380fe8b1dc32cf8f0b1b448470a77afb195438bafdf1d858bfb876f3edf7b", size = 8362801, upload-time = "2025-12-10T22:56:36.107Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/49/d651878698a0b67f23aa28e17f45a6d6dd3d3f933fa29087fa4ce5947b5a/matplotlib-3.10.8-cp314-cp314t-win_arm64.whl", hash = "sha256:113bb52413ea508ce954a02c10ffd0d565f9c3bc7f2eddc27dfe1731e71c7b5f", size = 8192560, upload-time = "2025-12-10T22:56:38.008Z" },
+]
+
+[[package]]
+name = "mcp"
+version = "1.16.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "httpx" },
+ { name = "httpx-sse" },
+ { name = "jsonschema" },
+ { name = "pydantic" },
+ { name = "pydantic-settings" },
+ { name = "python-multipart" },
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+ { name = "sse-starlette" },
+ { name = "starlette" },
+ { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/a1/b1f328da3b153683d2ec34f849b4b6eac2790fb240e3aef06ff2fab3df9d/mcp-1.16.0.tar.gz", hash = "sha256:39b8ca25460c578ee2cdad33feeea122694cfdf73eef58bee76c42f6ef0589df", size = 472918, upload-time = "2025-10-02T16:58:20.631Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/0e/7cebc88e17daf94ebe28c95633af595ccb2864dc2ee7abd75542d98495cc/mcp-1.16.0-py3-none-any.whl", hash = "sha256:ec917be9a5d31b09ba331e1768aa576e0af45470d657a0319996a20a57d7d633", size = 167266, upload-time = "2025-10-02T16:58:19.039Z" },
+]
+
+[[package]]
+name = "mdit-py-plugins"
+version = "0.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b2/fd/a756d36c0bfba5f6e39a1cdbdbfdd448dc02692467d83816dff4592a1ebc/mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6", size = 44655, upload-time = "2025-08-11T07:25:49.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/86/dd6e5db36df29e76c7a7699123569a4a18c1623ce68d826ed96c62643cae/mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f", size = 57205, upload-time = "2025-08-11T07:25:47.597Z" },
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
+]
+
+[[package]]
+name = "more-itertools"
+version = "10.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" },
+]
+
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+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.7.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" },
+ { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" },
+ { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" },
+ { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" },
+ { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" },
+ { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" },
+ { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" },
+ { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" },
+ { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" },
+ { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" },
+ { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" },
+ { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" },
+ { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" },
+ { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" },
+ { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" },
+ { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" },
+ { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" },
+ { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" },
+ { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" },
+ { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" },
+ { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" },
+ { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" },
+ { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" },
+ { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" },
+ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" },
+]
+
+[[package]]
+name = "multiprocess"
+version = "0.70.18"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "dill" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/fd/2ae3826f5be24c6ed87266bc4e59c46ea5b059a103f3d7e7eb76a52aeecb/multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d", size = 1798503, upload-time = "2025-04-17T03:11:27.742Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/d8/0cba6cf51a1a31f20471fbc823a716170c73012ddc4fb85d706630ed6e8f/multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea", size = 134948, upload-time = "2025-04-17T03:11:20.223Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/88/9039f2fed1012ef584751d4ceff9ab4a51e5ae264898f0b7cbf44340a859/multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d", size = 144462, upload-time = "2025-04-17T03:11:21.657Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/b6/5f922792be93b82ec6b5f270bbb1ef031fd0622847070bbcf9da816502cc/multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2", size = 150287, upload-time = "2025-04-17T03:11:22.69Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/25/7d7e78e750bc1aecfaf0efbf826c69a791d2eeaf29cf20cba93ff4cced78/multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334", size = 151917, upload-time = "2025-04-17T03:11:24.044Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/c3/ca84c19bd14cdfc21c388fdcebf08b86a7a470ebc9f5c3c084fc2dbc50f7/multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b", size = 132636, upload-time = "2025-04-17T03:11:24.936Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/28/dd72947e59a6a8c856448a5e74da6201cb5502ddff644fbc790e4bd40b9a/multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8", size = 133478, upload-time = "2025-04-17T03:11:26.253Z" },
+]
+
+[[package]]
+name = "murmurhash"
+version = "1.0.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/23/2e/88c147931ea9725d634840d538622e94122bceaf346233349b7b5c62964b/murmurhash-1.0.15.tar.gz", hash = "sha256:58e2b27b7847f9e2a6edf10b47a8c8dd70a4705f45dccb7bf76aeadacf56ba01", size = 13291, upload-time = "2025-11-14T09:51:15.272Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b6/46/be8522d3456fdccf1b8b049c6d82e7a3c1114c4fc2cfe14b04cba4b3e701/murmurhash-1.0.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d37e3ae44746bca80b1a917c2ea625cf216913564ed43f69d2888e5df97db0cb", size = 27884, upload-time = "2025-11-14T09:50:13.133Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/cc/630449bf4f6178d7daf948ce46ad00b25d279065fc30abd8d706be3d87e0/murmurhash-1.0.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0861cb11039409eaf46878456b7d985ef17b6b484103a6fc367b2ecec846891d", size = 27855, upload-time = "2025-11-14T09:50:14.859Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/30/ea8f601a9bf44db99468696efd59eb9cff1157cd55cb586d67116697583f/murmurhash-1.0.15-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5a301decfaccfec70fe55cb01dde2a012c3014a874542eaa7cc73477bb749616", size = 134088, upload-time = "2025-11-14T09:50:15.958Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/de/c40ce8c0877d406691e735b8d6e9c815f36a82b499d358313db5dbe219d7/murmurhash-1.0.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32c6fde7bd7e9407003370a07b5f4addacabe1556ad3dc2cac246b7a2bba3400", size = 133978, upload-time = "2025-11-14T09:50:17.572Z" },
+ { url = "https://files.pythonhosted.org/packages/47/84/bd49963ecd84ebab2fe66595e2d1ed41d5e8b5153af5dc930f0bd827007c/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d8b43a7011540dc3c7ce66f2134df9732e2bc3bbb4a35f6458bc755e48bde26", size = 132956, upload-time = "2025-11-14T09:50:18.742Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/7c/2530769c545074417c862583f05f4245644599f1e9ff619b3dfe2969aafc/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43bf4541892ecd95963fcd307bf1c575fc0fee1682f41c93007adee71ca2bb40", size = 134184, upload-time = "2025-11-14T09:50:19.941Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a4/b249b042f5afe34d14ada2dc4afc777e883c15863296756179652e081c44/murmurhash-1.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:f4ac15a2089dc42e6eb0966622d42d2521590a12c92480aafecf34c085302cca", size = 25647, upload-time = "2025-11-14T09:50:21.049Z" },
+ { url = "https://files.pythonhosted.org/packages/13/bf/028179259aebc18fd4ba5cae2601d1d47517427a537ab44336446431a215/murmurhash-1.0.15-cp312-cp312-win_arm64.whl", hash = "sha256:4a70ca4ae19e600d9be3da64d00710e79dde388a4d162f22078d64844d0ebdda", size = 23338, upload-time = "2025-11-14T09:50:22.359Z" },
+ { url = "https://files.pythonhosted.org/packages/29/2f/ba300b5f04dae0409202d6285668b8a9d3ade43a846abee3ef611cb388d5/murmurhash-1.0.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fe50dc70e52786759358fd1471e309b94dddfffb9320d9dfea233c7684c894ba", size = 27861, upload-time = "2025-11-14T09:50:23.804Z" },
+ { url = "https://files.pythonhosted.org/packages/34/02/29c19d268e6f4ea1ed2a462c901eed1ed35b454e2cbc57da592fad663ac6/murmurhash-1.0.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1349a7c23f6092e7998ddc5bd28546cc31a595afc61e9fdb3afc423feec3d7ad", size = 27840, upload-time = "2025-11-14T09:50:25.146Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/63/58e2de2b5232cd294c64092688c422196e74f9fa8b3958bdf02d33df24b9/murmurhash-1.0.15-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ba6d05de2613535b5a9227d4ad8ef40a540465f64660d4a8800634ae10e04f", size = 133080, upload-time = "2025-11-14T09:50:26.566Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/9a/d13e2e9f8ba1ced06840921a50f7cece0a475453284158a3018b72679761/murmurhash-1.0.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fa1b70b3cc2801ab44179c65827bbd12009c68b34e9d9ce7125b6a0bd35af63c", size = 132648, upload-time = "2025-11-14T09:50:27.788Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e1/47994f1813fa205c84977b0ff51ae6709f8539af052c7491a5f863d82bdc/murmurhash-1.0.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:213d710fb6f4ef3bc11abbfad0fa94a75ffb675b7dc158c123471e5de869f9af", size = 131502, upload-time = "2025-11-14T09:50:29.339Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ea/90c1fd00b4aeb704fb5e84cd666b33ffd7f245155048071ffbb51d2bb57d/murmurhash-1.0.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b65a5c4e7f5d71f7ccac2d2b60bdf7092d7976270878cfec59d5a66a533db823", size = 132736, upload-time = "2025-11-14T09:50:30.545Z" },
+ { url = "https://files.pythonhosted.org/packages/00/db/da73462dbfa77f6433b128d2120ba7ba300f8c06dc4f4e022c38d240a5f5/murmurhash-1.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:9aba94c5d841e1904cd110e94ceb7f49cfb60a874bbfb27e0373622998fb7c7c", size = 25682, upload-time = "2025-11-14T09:50:31.624Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/83/032729ef14971b938fbef41ee125fc8800020ee229bd35178b6ede8ee934/murmurhash-1.0.15-cp313-cp313-win_arm64.whl", hash = "sha256:263807eca40d08c7b702413e45cca75ecb5883aa337237dc5addb660f1483378", size = 23370, upload-time = "2025-11-14T09:50:33.264Z" },
+ { url = "https://files.pythonhosted.org/packages/10/83/7547d9205e9bd2f8e5dfd0b682cc9277594f98909f228eb359489baec1df/murmurhash-1.0.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:694fd42a74b7ce257169d14c24aa616aa6cd4ccf8abe50eca0557e08da99d055", size = 29955, upload-time = "2025-11-14T09:50:34.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/c7/3afd5de7a5b3ae07fe2d3a3271b327ee1489c58ba2b2f2159bd31a25edb9/murmurhash-1.0.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a2ea4546ba426390beff3cd10db8f0152fdc9072c4f2583ec7d8aa9f3e4ac070", size = 30108, upload-time = "2025-11-14T09:50:35.53Z" },
+ { url = "https://files.pythonhosted.org/packages/02/69/d6637ee67d78ebb2538c00411f28ea5c154886bbe1db16c49435a8a4ab16/murmurhash-1.0.15-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:34e5a91139c40b10f98d0b297907f5d5267b4b1b2e5dd2eb74a021824f751b98", size = 164054, upload-time = "2025-11-14T09:50:36.591Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/4c/89e590165b4c7da6bf941441212a721a270195332d3aacfdfdf527d466ca/murmurhash-1.0.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc35606868a5961cf42e79314ca0bddf5a400ce377b14d83192057928d6252ec", size = 168153, upload-time = "2025-11-14T09:50:37.856Z" },
+ { url = "https://files.pythonhosted.org/packages/07/7a/95c42df0c21d2e413b9fcd17317a7587351daeb264dc29c6aec1fdbd26f8/murmurhash-1.0.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:43cc6ac3b91ca0f7a5ae9c063ba4d6c26972c97fd7c25280ecc666413e4c5535", size = 164345, upload-time = "2025-11-14T09:50:39.346Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/22/9d02c880a88b83bb3ce7d6a38fb727373ab78d82e5f3d8d9fc5612219f90/murmurhash-1.0.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:847d712136cb462f0e4bd6229ee2d9eb996d8854eb8312dff3d20c8f5181fda5", size = 161990, upload-time = "2025-11-14T09:50:40.689Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/750232524e0dc262e8dcede6536dafc766faadd9a52f1d23746b02948ad8/murmurhash-1.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:2680851af6901dbe66cc4aa7ef8e263de47e6e1b425ae324caa571bdf18f8d58", size = 28812, upload-time = "2025-11-14T09:50:41.971Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/89/4ad9d215ef6ade89f27a72dc4e86b98ef1a43534cc3e6a6900a362a0bf0a/murmurhash-1.0.15-cp313-cp313t-win_arm64.whl", hash = "sha256:189a8de4d657b5da9efd66601b0636330b08262b3a55431f2379097c986995d0", size = 25398, upload-time = "2025-11-14T09:50:43.023Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/69/726df275edf07688146966e15eaaa23168100b933a2e1a29b37eb56c6db8/murmurhash-1.0.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7c4280136b738e85ff76b4bdc4341d0b867ee753e73fd8b6994288080c040d0b", size = 28029, upload-time = "2025-11-14T09:50:44.124Z" },
+ { url = "https://files.pythonhosted.org/packages/59/8f/24ecf9061bc2b20933df8aba47c73e904274ea8811c8300cab92f6f82372/murmurhash-1.0.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4d681f474830489e2ec1d912095cfff027fbaf2baa5414c7e9d25b89f0fab68", size = 27912, upload-time = "2025-11-14T09:50:45.266Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/26/fff3caba25aa3c0622114e03c69fb66c839b22335b04d7cce91a3a126d44/murmurhash-1.0.15-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d7e47c5746785db6a43b65fac47b9e63dd71dfbd89a8c92693425b9715e68c6e", size = 131847, upload-time = "2025-11-14T09:50:46.819Z" },
+ { url = "https://files.pythonhosted.org/packages/df/e4/0f2b9fc533467a27afb4e906c33f32d5f637477de87dd94690e0c44335a6/murmurhash-1.0.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e8e674f02a99828c8a671ba99cd03299381b2f0744e6f25c29cadfc6151dc724", size = 132267, upload-time = "2025-11-14T09:50:48.298Z" },
+ { url = "https://files.pythonhosted.org/packages/da/bf/9d1c107989728ec46e25773d503aa54070b32822a18cfa7f9d5f41bc17a5/murmurhash-1.0.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:26fd7c7855ac4850ad8737991d7b0e3e501df93ebaf0cf45aa5954303085fdba", size = 131894, upload-time = "2025-11-14T09:50:49.485Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/81/dcf27c71445c0e993b10e33169a098ca60ee702c5c58fcbde205fa6332a6/murmurhash-1.0.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cb8ebafae60d5f892acff533cc599a359954d8c016a829514cb3f6e9ee10f322", size = 132054, upload-time = "2025-11-14T09:50:50.747Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/32/e874a14b2d2246bd2d16f80f49fad393a3865d4ee7d66d2cae939a67a29a/murmurhash-1.0.15-cp314-cp314-win_amd64.whl", hash = "sha256:898a629bf111f1aeba4437e533b5b836c0a9d2dd12d6880a9c75f6ca13e30e22", size = 26579, upload-time = "2025-11-14T09:50:52.278Z" },
+ { url = "https://files.pythonhosted.org/packages/af/8e/4fca051ed8ae4d23a15aaf0a82b18cb368e8cf84f1e3b474d5749ec46069/murmurhash-1.0.15-cp314-cp314-win_arm64.whl", hash = "sha256:88dc1dd53b7b37c0df1b8b6bce190c12763014492f0269ff7620dc6027f470f4", size = 24341, upload-time = "2025-11-14T09:50:53.295Z" },
+ { url = "https://files.pythonhosted.org/packages/38/9c/c72c2a4edd86aac829337ab9f83cf04cdb15e5d503e4c9a3a243f30a261c/murmurhash-1.0.15-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6cb4e962ec4f928b30c271b2d84e6707eff6d942552765b663743cfa618b294b", size = 30146, upload-time = "2025-11-14T09:50:54.705Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/d7/72b47ebc86436cd0aa1fd4c6e8779521ec389397ac11389990278d0f7a47/murmurhash-1.0.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5678a3ea4fbf0cbaaca2bed9b445f556f294d5f799c67185d05ffcb221a77faf", size = 30141, upload-time = "2025-11-14T09:50:55.829Z" },
+ { url = "https://files.pythonhosted.org/packages/64/bb/6d2f09135079c34dc2d26e961c52742d558b320c61503f273eab6ba743d9/murmurhash-1.0.15-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ef19f38c6b858eef83caf710773db98c8f7eb2193b4c324650c74f3d8ba299e0", size = 163898, upload-time = "2025-11-14T09:50:56.946Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/e2/9c1b462e33f9cb2d632056f07c90b502fc20bd7da50a15d0557343bd2fed/murmurhash-1.0.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22aa3ceaedd2e57078b491ed08852d512b84ff4ff9bb2ff3f9bf0eec7f214c9e", size = 168040, upload-time = "2025-11-14T09:50:58.234Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/73/8694db1408fcdfa73589f7df6c445437ea146986fa1e393ec60d26d6e30c/murmurhash-1.0.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bba0e0262c0d08682b028cb963ac477bd9839029486fa1333fc5c01fb6072749", size = 164239, upload-time = "2025-11-14T09:50:59.95Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/f9/8e360bdfc3c44e267e7e046f0e0b9922766da92da26959a6963f597e6bb5/murmurhash-1.0.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fd8189ee293a09f30f4931408f40c28ccd42d9de4f66595f8814879339378bc", size = 161811, upload-time = "2025-11-14T09:51:01.289Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/31/97649680595b1096803d877ababb9a67c07f4378f177ec885eea28b9db6d/murmurhash-1.0.15-cp314-cp314t-win_amd64.whl", hash = "sha256:66395b1388f7daa5103db92debe06842ae3be4c0749ef6db68b444518666cdcc", size = 29817, upload-time = "2025-11-14T09:51:02.493Z" },
+ { url = "https://files.pythonhosted.org/packages/76/66/4fce8755f25d77324401886c00017c556be7ca3039575b94037aff905385/murmurhash-1.0.15-cp314-cp314t-win_arm64.whl", hash = "sha256:c22e56c6a0b70598a66e456de5272f76088bc623688da84ef403148a6d41851d", size = 26219, upload-time = "2025-11-14T09:51:03.563Z" },
+]
+
+[[package]]
+name = "neo4j"
+version = "6.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pytz" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/1b/01/d6ce65e4647f6cb2b9cca3b813978f7329b54b4e36660aaec1ddf0ccce7a/neo4j-6.1.0.tar.gz", hash = "sha256:b5dde8c0d8481e7b6ae3733569d990dd3e5befdc5d452f531ad1884ed3500b84", size = 239629, upload-time = "2026-01-12T11:27:34.777Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/70/5c/ee71e2dd955045425ef44283f40ba1da67673cf06404916ca2950ac0cd39/neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d", size = 325326, upload-time = "2026-01-12T11:27:33.196Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320, upload-time = "2026-01-10T06:44:59.619Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888, upload-time = "2026-01-10T06:42:40.913Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956, upload-time = "2026-01-10T06:42:43.091Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567, upload-time = "2026-01-10T06:42:45.107Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459, upload-time = "2026-01-10T06:42:48.152Z" },
+ { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859, upload-time = "2026-01-10T06:42:49.947Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419, upload-time = "2026-01-10T06:42:52.409Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131, upload-time = "2026-01-10T06:42:54.694Z" },
+ { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342, upload-time = "2026-01-10T06:42:56.991Z" },
+ { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015, upload-time = "2026-01-10T06:42:59.631Z" },
+ { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730, upload-time = "2026-01-10T06:43:01.627Z" },
+ { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166, upload-time = "2026-01-10T06:43:03.673Z" },
+ { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495, upload-time = "2026-01-10T06:43:06.283Z" },
+ { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657, upload-time = "2026-01-10T06:43:09.094Z" },
+ { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256, upload-time = "2026-01-10T06:43:13.634Z" },
+ { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212, upload-time = "2026-01-10T06:43:15.661Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871, upload-time = "2026-01-10T06:43:17.324Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305, upload-time = "2026-01-10T06:43:19.376Z" },
+ { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909, upload-time = "2026-01-10T06:43:21.808Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380, upload-time = "2026-01-10T06:43:23.957Z" },
+ { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089, upload-time = "2026-01-10T06:43:27.535Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230, upload-time = "2026-01-10T06:43:29.298Z" },
+ { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125, upload-time = "2026-01-10T06:43:31.782Z" },
+ { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156, upload-time = "2026-01-10T06:43:34.237Z" },
+ { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663, upload-time = "2026-01-10T06:43:36.211Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224, upload-time = "2026-01-10T06:43:37.884Z" },
+ { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352, upload-time = "2026-01-10T06:43:39.479Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279, upload-time = "2026-01-10T06:43:41.93Z" },
+ { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316, upload-time = "2026-01-10T06:43:44.121Z" },
+ { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884, upload-time = "2026-01-10T06:43:46.613Z" },
+ { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138, upload-time = "2026-01-10T06:43:48.854Z" },
+ { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478, upload-time = "2026-01-10T06:43:50.476Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981, upload-time = "2026-01-10T06:43:52.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046, upload-time = "2026-01-10T06:43:54.797Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858, upload-time = "2026-01-10T06:43:57.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417, upload-time = "2026-01-10T06:43:59.037Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643, upload-time = "2026-01-10T06:44:01.852Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963, upload-time = "2026-01-10T06:44:04.047Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811, upload-time = "2026-01-10T06:44:06.207Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643, upload-time = "2026-01-10T06:44:08.33Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601, upload-time = "2026-01-10T06:44:10.841Z" },
+ { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722, upload-time = "2026-01-10T06:44:13.332Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590, upload-time = "2026-01-10T06:44:15.006Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180, upload-time = "2026-01-10T06:44:17.386Z" },
+ { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774, upload-time = "2026-01-10T06:44:19.467Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274, upload-time = "2026-01-10T06:44:23.189Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306, upload-time = "2026-01-10T06:44:25.012Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653, upload-time = "2026-01-10T06:44:26.706Z" },
+ { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144, upload-time = "2026-01-10T06:44:29.378Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425, upload-time = "2026-01-10T06:44:31.721Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053, upload-time = "2026-01-10T06:44:34.617Z" },
+ { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482, upload-time = "2026-01-10T06:44:36.798Z" },
+ { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117, upload-time = "2026-01-10T06:44:38.828Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121, upload-time = "2026-01-10T06:44:41.644Z" },
+]
+
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.8.4.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.8.93"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "9.10.2.21"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
+]
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.3.3.83"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
+]
+
+[[package]]
+name = "nvidia-cufile-cu12"
+version = "1.13.1.3"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" },
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.9.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" },
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.7.3.90"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-cusparse-cu12", marker = "sys_platform == 'linux'" },
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
+]
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.5.8.93"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", marker = "sys_platform == 'linux'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
+]
+
+[[package]]
+name = "nvidia-cusparselt-cu12"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.27.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" },
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.8.93"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" },
+]
+
+[[package]]
+name = "nvidia-nvshmem-cu12"
+version = "3.4.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" },
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" },
+]
+
+[[package]]
+name = "onnxruntime"
+version = "1.20.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coloredlogs" },
+ { name = "flatbuffers" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "protobuf" },
+ { name = "sympy" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/39/9335e0874f68f7d27103cbffc0e235e32e26759202df6085716375c078bb/onnxruntime-1.20.1-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:22b0655e2bf4f2161d52706e31f517a0e54939dc393e92577df51808a7edc8c9", size = 31007580, upload-time = "2024-11-21T00:49:07.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/9d/a42a84e10f1744dd27c6f2f9280cc3fb98f869dd19b7cd042e391ee2ab61/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f56e898815963d6dc4ee1c35fc6c36506466eff6d16f3cb9848cea4e8c8172", size = 11952833, upload-time = "2024-11-21T00:49:10.563Z" },
+ { url = "https://files.pythonhosted.org/packages/47/42/2f71f5680834688a9c81becbe5c5bb996fd33eaed5c66ae0606c3b1d6a02/onnxruntime-1.20.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e", size = 13333903, upload-time = "2024-11-21T00:49:12.984Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/f1/aabfdf91d013320aa2fc46cf43c88ca0182860ff15df872b4552254a9680/onnxruntime-1.20.1-cp312-cp312-win32.whl", hash = "sha256:bd386cc9ee5f686ee8a75ba74037750aca55183085bf1941da8efcfe12d5b120", size = 9814562, upload-time = "2024-11-21T00:49:15.453Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/80/76979e0b744307d488c79e41051117634b956612cc731f1028eb17ee7294/onnxruntime-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:19c2d843eb074f385e8bbb753a40df780511061a63f9def1b216bf53860223fb", size = 11331482, upload-time = "2024-11-21T00:49:19.412Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/71/c5d980ac4189589267a06f758bd6c5667d07e55656bed6c6c0580733ad07/onnxruntime-1.20.1-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:cc01437a32d0042b606f462245c8bbae269e5442797f6213e36ce61d5abdd8cc", size = 31007574, upload-time = "2024-11-21T00:49:23.225Z" },
+ { url = "https://files.pythonhosted.org/packages/81/0d/13bbd9489be2a6944f4a940084bfe388f1100472f38c07080a46fbd4ab96/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb44b08e017a648924dbe91b82d89b0c105b1adcfe31e90d1dc06b8677ad37be", size = 11951459, upload-time = "2024-11-21T00:49:26.269Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ea/4454ae122874fd52bbb8a961262de81c5f932edeb1b72217f594c700d6ef/onnxruntime-1.20.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bda6aebdf7917c1d811f21d41633df00c58aff2bef2f598f69289c1f1dabc4b3", size = 13331620, upload-time = "2024-11-21T00:49:28.875Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/e0/50db43188ca1c945decaa8fc2a024c33446d31afed40149897d4f9de505f/onnxruntime-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:d30367df7e70f1d9fc5a6a68106f5961686d39b54d3221f760085524e8d38e16", size = 11331758, upload-time = "2024-11-21T00:49:31.417Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/55/3821c5fd60b52a6c82a00bba18531793c93c4addfe64fbf061e235c5617a/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9158465745423b2b5d97ed25aa7740c7d38d2993ee2e5c3bfacb0c4145c49d8", size = 11950342, upload-time = "2024-11-21T00:49:34.164Z" },
+ { url = "https://files.pythonhosted.org/packages/14/56/fd990ca222cef4f9f4a9400567b9a15b220dee2eafffb16b2adbc55c8281/onnxruntime-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0df6f2df83d61f46e842dbcde610ede27218947c33e994545a22333491e72a3b", size = 13337040, upload-time = "2024-11-21T00:49:37.271Z" },
+]
+
+[[package]]
+name = "openai"
+version = "1.109.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" },
+]
+
+[[package]]
+name = "openapi-core"
+version = "0.22.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "isodate" },
+ { name = "jsonschema" },
+ { name = "jsonschema-path" },
+ { name = "more-itertools" },
+ { name = "openapi-schema-validator" },
+ { name = "openapi-spec-validator" },
+ { name = "typing-extensions" },
+ { name = "werkzeug" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/fd/65/ee75f25b9459a02df6f713f8ffde5dacb57b8b4e45145cde4cab28b5abba/openapi_core-0.22.0.tar.gz", hash = "sha256:b30490dfa74e3aac2276105525590135212352f5dd7e5acf8f62f6a89ed6f2d0", size = 109242, upload-time = "2025-12-22T19:19:49.608Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/8e/a1bf9e7d1b170122aa33b6cf2c788b68824712427deb19795eb7db1b8dd5/openapi_core-0.22.0-py3-none-any.whl", hash = "sha256:8fb7c325f2db4ef6c60584b1870f90eeb3183aa47e30643715c5003b7677a149", size = 108384, upload-time = "2025-12-22T19:19:47.904Z" },
+]
+
+[[package]]
+name = "openapi-pydantic"
+version = "0.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/2e/58d83848dd1a79cb92ed8e63f6ba901ca282c5f09d04af9423ec26c56fd7/openapi_pydantic-0.5.1.tar.gz", hash = "sha256:ff6835af6bde7a459fb93eb93bb92b8749b754fc6e51b2f1590a19dc3005ee0d", size = 60892, upload-time = "2025-01-08T19:29:27.083Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" },
+]
+
+[[package]]
+name = "openapi-schema-validator"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "jsonschema-specifications" },
+ { name = "rfc3339-validator" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" },
+]
+
+[[package]]
+name = "openapi-spec-validator"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jsonschema" },
+ { name = "jsonschema-path" },
+ { name = "lazy-object-proxy" },
+ { name = "openapi-schema-validator" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" },
+]
+
+[[package]]
+name = "orjson"
+version = "3.11.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/b8/333fdb27840f3bf04022d21b654a35f58e15407183aeb16f3b41aa053446/orjson-3.11.5.tar.gz", hash = "sha256:82393ab47b4fe44ffd0a7659fa9cfaacc717eb617c93cde83795f14af5c2e9d5", size = 5972347, upload-time = "2025-12-06T15:55:39.458Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/a4/8052a029029b096a78955eadd68ab594ce2197e24ec50e6b6d2ab3f4e33b/orjson-3.11.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:334e5b4bff9ad101237c2d799d9fd45737752929753bf4faf4b207335a416b7d", size = 245347, upload-time = "2025-12-06T15:54:22.061Z" },
+ { url = "https://files.pythonhosted.org/packages/64/67/574a7732bd9d9d79ac620c8790b4cfe0717a3d5a6eb2b539e6e8995e24a0/orjson-3.11.5-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:ff770589960a86eae279f5d8aa536196ebda8273a2a07db2a54e82b93bc86626", size = 129435, upload-time = "2025-12-06T15:54:23.615Z" },
+ { url = "https://files.pythonhosted.org/packages/52/8d/544e77d7a29d90cf4d9eecd0ae801c688e7f3d1adfa2ebae5e1e94d38ab9/orjson-3.11.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed24250e55efbcb0b35bed7caaec8cedf858ab2f9f2201f17b8938c618c8ca6f", size = 132074, upload-time = "2025-12-06T15:54:24.694Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/57/b9f5b5b6fbff9c26f77e785baf56ae8460ef74acdb3eae4931c25b8f5ba9/orjson-3.11.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a66d7769e98a08a12a139049aac2f0ca3adae989817f8c43337455fbc7669b85", size = 130520, upload-time = "2025-12-06T15:54:26.185Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6d/d34970bf9eb33f9ec7c979a262cad86076814859e54eb9a059a52f6dc13d/orjson-3.11.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:86cfc555bfd5794d24c6a1903e558b50644e5e68e6471d66502ce5cb5fdef3f9", size = 136209, upload-time = "2025-12-06T15:54:27.264Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/39/bc373b63cc0e117a105ea12e57280f83ae52fdee426890d57412432d63b3/orjson-3.11.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a230065027bc2a025e944f9d4714976a81e7ecfa940923283bca7bbc1f10f626", size = 139837, upload-time = "2025-12-06T15:54:28.75Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/aa/7c4818c8d7d324da220f4f1af55c343956003aa4d1ce1857bdc1d396ba69/orjson-3.11.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b29d36b60e606df01959c4b982729c8845c69d1963f88686608be9ced96dbfaa", size = 137307, upload-time = "2025-12-06T15:54:29.856Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bf/0993b5a056759ba65145effe3a79dd5a939d4a070eaa5da2ee3180fbb13f/orjson-3.11.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74099c6b230d4261fdc3169d50efc09abf38ace1a42ea2f9994b1d79153d477", size = 139020, upload-time = "2025-12-06T15:54:31.024Z" },
+ { url = "https://files.pythonhosted.org/packages/65/e8/83a6c95db3039e504eda60fc388f9faedbb4f6472f5aba7084e06552d9aa/orjson-3.11.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e697d06ad57dd0c7a737771d470eedc18e68dfdefcdd3b7de7f33dfda5b6212e", size = 141099, upload-time = "2025-12-06T15:54:32.196Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b4/24fdc024abfce31c2f6812973b0a693688037ece5dc64b7a60c1ce69e2f2/orjson-3.11.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e08ca8a6c851e95aaecc32bc44a5aa75d0ad26af8cdac7c77e4ed93acf3d5b69", size = 413540, upload-time = "2025-12-06T15:54:33.361Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/37/01c0ec95d55ed0c11e4cae3e10427e479bba40c77312b63e1f9665e0737d/orjson-3.11.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8b5f96c05fce7d0218df3fdfeb962d6b8cfff7e3e20264306b46dd8b217c0f3", size = 151530, upload-time = "2025-12-06T15:54:34.6Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/d4/f9ebc57182705bb4bbe63f5bbe14af43722a2533135e1d2fb7affa0c355d/orjson-3.11.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ddbfdb5099b3e6ba6d6ea818f61997bb66de14b411357d24c4612cf1ebad08ca", size = 141863, upload-time = "2025-12-06T15:54:35.801Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/04/02102b8d19fdcb009d72d622bb5781e8f3fae1646bf3e18c53d1bc8115b5/orjson-3.11.5-cp312-cp312-win32.whl", hash = "sha256:9172578c4eb09dbfcf1657d43198de59b6cef4054de385365060ed50c458ac98", size = 135255, upload-time = "2025-12-06T15:54:37.209Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fb/f05646c43d5450492cb387de5549f6de90a71001682c17882d9f66476af5/orjson-3.11.5-cp312-cp312-win_amd64.whl", hash = "sha256:2b91126e7b470ff2e75746f6f6ee32b9ab67b7a93c8ba1d15d3a0caaf16ec875", size = 133252, upload-time = "2025-12-06T15:54:38.401Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/a6/7b8c0b26ba18c793533ac1cd145e131e46fcf43952aa94c109b5b913c1f0/orjson-3.11.5-cp312-cp312-win_arm64.whl", hash = "sha256:acbc5fac7e06777555b0722b8ad5f574739e99ffe99467ed63da98f97f9ca0fe", size = 126777, upload-time = "2025-12-06T15:54:39.515Z" },
+ { url = "https://files.pythonhosted.org/packages/10/43/61a77040ce59f1569edf38f0b9faadc90c8cf7e9bec2e0df51d0132c6bb7/orjson-3.11.5-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3b01799262081a4c47c035dd77c1301d40f568f77cc7ec1bb7db5d63b0a01629", size = 245271, upload-time = "2025-12-06T15:54:40.878Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f9/0f79be617388227866d50edd2fd320cb8fb94dc1501184bb1620981a0aba/orjson-3.11.5-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:61de247948108484779f57a9f406e4c84d636fa5a59e411e6352484985e8a7c3", size = 129422, upload-time = "2025-12-06T15:54:42.403Z" },
+ { url = "https://files.pythonhosted.org/packages/77/42/f1bf1549b432d4a78bfa95735b79b5dac75b65b5bb815bba86ad406ead0a/orjson-3.11.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:894aea2e63d4f24a7f04a1908307c738d0dce992e9249e744b8f4e8dd9197f39", size = 132060, upload-time = "2025-12-06T15:54:43.531Z" },
+ { url = "https://files.pythonhosted.org/packages/25/49/825aa6b929f1a6ed244c78acd7b22c1481fd7e5fda047dc8bf4c1a807eb6/orjson-3.11.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ddc21521598dbe369d83d4d40338e23d4101dad21dae0e79fa20465dbace019f", size = 130391, upload-time = "2025-12-06T15:54:45.059Z" },
+ { url = "https://files.pythonhosted.org/packages/42/ec/de55391858b49e16e1aa8f0bbbb7e5997b7345d8e984a2dec3746d13065b/orjson-3.11.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cce16ae2f5fb2c53c3eafdd1706cb7b6530a67cc1c17abe8ec747f5cd7c0c51", size = 135964, upload-time = "2025-12-06T15:54:46.576Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/40/820bc63121d2d28818556a2d0a09384a9f0262407cf9fa305e091a8048df/orjson-3.11.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e46c762d9f0e1cfb4ccc8515de7f349abbc95b59cb5a2bd68df5973fdef913f8", size = 139817, upload-time = "2025-12-06T15:54:48.084Z" },
+ { url = "https://files.pythonhosted.org/packages/09/c7/3a445ca9a84a0d59d26365fd8898ff52bdfcdcb825bcc6519830371d2364/orjson-3.11.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d7345c759276b798ccd6d77a87136029e71e66a8bbf2d2755cbdde1d82e78706", size = 137336, upload-time = "2025-12-06T15:54:49.426Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/b3/dc0d3771f2e5d1f13368f56b339c6782f955c6a20b50465a91acb79fe961/orjson-3.11.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75bc2e59e6a2ac1dd28901d07115abdebc4563b5b07dd612bf64260a201b1c7f", size = 138993, upload-time = "2025-12-06T15:54:50.939Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a2/65267e959de6abe23444659b6e19c888f242bf7725ff927e2292776f6b89/orjson-3.11.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:54aae9b654554c3b4edd61896b978568c6daa16af96fa4681c9b5babd469f863", size = 141070, upload-time = "2025-12-06T15:54:52.414Z" },
+ { url = "https://files.pythonhosted.org/packages/63/c9/da44a321b288727a322c6ab17e1754195708786a04f4f9d2220a5076a649/orjson-3.11.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4bdd8d164a871c4ec773f9de0f6fe8769c2d6727879c37a9666ba4183b7f8228", size = 413505, upload-time = "2025-12-06T15:54:53.67Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/17/68dc14fa7000eefb3d4d6d7326a190c99bb65e319f02747ef3ebf2452f12/orjson-3.11.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a261fef929bcf98a60713bf5e95ad067cea16ae345d9a35034e73c3990e927d2", size = 151342, upload-time = "2025-12-06T15:54:55.113Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c5/ccee774b67225bed630a57478529fc026eda33d94fe4c0eac8fe58d4aa52/orjson-3.11.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c028a394c766693c5c9909dec76b24f37e6a1b91999e8d0c0d5feecbe93c3e05", size = 141823, upload-time = "2025-12-06T15:54:56.331Z" },
+ { url = "https://files.pythonhosted.org/packages/67/80/5d00e4155d0cd7390ae2087130637671da713959bb558db9bac5e6f6b042/orjson-3.11.5-cp313-cp313-win32.whl", hash = "sha256:2cc79aaad1dfabe1bd2d50ee09814a1253164b3da4c00a78c458d82d04b3bdef", size = 135236, upload-time = "2025-12-06T15:54:57.507Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fe/792cc06a84808dbdc20ac6eab6811c53091b42f8e51ecebf14b540e9cfe4/orjson-3.11.5-cp313-cp313-win_amd64.whl", hash = "sha256:ff7877d376add4e16b274e35a3f58b7f37b362abf4aa31863dadacdd20e3a583", size = 133167, upload-time = "2025-12-06T15:54:58.71Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2c/d158bd8b50e3b1cfdcf406a7e463f6ffe3f0d167b99634717acdaf5e299f/orjson-3.11.5-cp313-cp313-win_arm64.whl", hash = "sha256:59ac72ea775c88b163ba8d21b0177628bd015c5dd060647bbab6e22da3aad287", size = 126712, upload-time = "2025-12-06T15:54:59.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/60/77d7b839e317ead7bb225d55bb50f7ea75f47afc489c81199befc5435b50/orjson-3.11.5-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e446a8ea0a4c366ceafc7d97067bfd55292969143b57e3c846d87fc701e797a0", size = 245252, upload-time = "2025-12-06T15:55:01.127Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/aa/d4639163b400f8044cef0fb9aa51b0337be0da3a27187a20d1166e742370/orjson-3.11.5-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:53deb5addae9c22bbe3739298f5f2196afa881ea75944e7720681c7080909a81", size = 129419, upload-time = "2025-12-06T15:55:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/30/94/9eabf94f2e11c671111139edf5ec410d2f21e6feee717804f7e8872d883f/orjson-3.11.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82cd00d49d6063d2b8791da5d4f9d20539c5951f965e45ccf4e96d33505ce68f", size = 132050, upload-time = "2025-12-06T15:55:03.918Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/c8/ca10f5c5322f341ea9a9f1097e140be17a88f88d1cfdd29df522970d9744/orjson-3.11.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3fd15f9fc8c203aeceff4fda211157fad114dde66e92e24097b3647a08f4ee9e", size = 130370, upload-time = "2025-12-06T15:55:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d4/e96824476d361ee2edd5c6290ceb8d7edf88d81148a6ce172fc00278ca7f/orjson-3.11.5-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9df95000fbe6777bf9820ae82ab7578e8662051bb5f83d71a28992f539d2cda7", size = 136012, upload-time = "2025-12-06T15:55:06.402Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8e/9bc3423308c425c588903f2d103cfcfe2539e07a25d6522900645a6f257f/orjson-3.11.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a8d676748fca47ade5bc3da7430ed7767afe51b2f8100e3cd65e151c0eaceb", size = 139809, upload-time = "2025-12-06T15:55:07.656Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3c/b404e94e0b02a232b957c54643ce68d0268dacb67ac33ffdee24008c8b27/orjson-3.11.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa0f513be38b40234c77975e68805506cad5d57b3dfd8fe3baa7f4f4051e15b4", size = 137332, upload-time = "2025-12-06T15:55:08.961Z" },
+ { url = "https://files.pythonhosted.org/packages/51/30/cc2d69d5ce0ad9b84811cdf4a0cd5362ac27205a921da524ff42f26d65e0/orjson-3.11.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa1863e75b92891f553b7922ce4ee10ed06db061e104f2b7815de80cdcb135ad", size = 138983, upload-time = "2025-12-06T15:55:10.595Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/87/de3223944a3e297d4707d2fe3b1ffb71437550e165eaf0ca8bbe43ccbcb1/orjson-3.11.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4be86b58e9ea262617b8ca6251a2f0d63cc132a6da4b5fcc8e0a4128782c829", size = 141069, upload-time = "2025-12-06T15:55:11.832Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/81d5087ae74be33bcae3ff2d80f5ccaa4a8fedc6d39bf65a427a95b8977f/orjson-3.11.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:b923c1c13fa02084eb38c9c065afd860a5cff58026813319a06949c3af5732ac", size = 413491, upload-time = "2025-12-06T15:55:13.314Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/6f/f6058c21e2fc1efaf918986dbc2da5cd38044f1a2d4b7b91ad17c4acf786/orjson-3.11.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1b6bd351202b2cd987f35a13b5e16471cf4d952b42a73c391cc537974c43ef6d", size = 151375, upload-time = "2025-12-06T15:55:14.715Z" },
+ { url = "https://files.pythonhosted.org/packages/54/92/c6921f17d45e110892899a7a563a925b2273d929959ce2ad89e2525b885b/orjson-3.11.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb150d529637d541e6af06bbe3d02f5498d628b7f98267ff87647584293ab439", size = 141850, upload-time = "2025-12-06T15:55:15.94Z" },
+ { url = "https://files.pythonhosted.org/packages/88/86/cdecb0140a05e1a477b81f24739da93b25070ee01ce7f7242f44a6437594/orjson-3.11.5-cp314-cp314-win32.whl", hash = "sha256:9cc1e55c884921434a84a0c3dd2699eb9f92e7b441d7f53f3941079ec6ce7499", size = 135278, upload-time = "2025-12-06T15:55:17.202Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/97/b638d69b1e947d24f6109216997e38922d54dcdcdb1b11c18d7efd2d3c59/orjson-3.11.5-cp314-cp314-win_amd64.whl", hash = "sha256:a4f3cb2d874e03bc7767c8f88adaa1a9a05cecea3712649c3b58589ec7317310", size = 133170, upload-time = "2025-12-06T15:55:18.468Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/dd/f4fff4a6fe601b4f8f3ba3aa6da8ac33d17d124491a3b804c662a70e1636/orjson-3.11.5-cp314-cp314-win_arm64.whl", hash = "sha256:38b22f476c351f9a1c43e5b07d8b5a02eb24a6ab8e75f700f7d479d4568346a5", size = 126713, upload-time = "2025-12-06T15:55:19.738Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+]
+
+[[package]]
+name = "pandas"
+version = "2.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "python-dateutil" },
+ { name = "pytz" },
+ { name = "tzdata" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
+ { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
+ { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
+ { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
+ { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
+ { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
+ { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
+ { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
+ { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
+ { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
+ { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
+ { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
+ { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
+ { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
+ { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
+ { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
+]
+
+[[package]]
+name = "pathable"
+version = "0.4.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
+]
+
+[[package]]
+name = "pdfminer-six"
+version = "20260107"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "charset-normalizer" },
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/34/a4/5cec1112009f0439a5ca6afa8ace321f0ab2f48da3255b7a1c8953014670/pdfminer_six-20260107.tar.gz", hash = "sha256:96bfd431e3577a55a0efd25676968ca4ce8fd5b53f14565f85716ff363889602", size = 8512094, upload-time = "2026-01-07T13:29:12.937Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/20/8b/28c4eaec9d6b036a52cb44720408f26b1a143ca9bce76cc19e8f5de00ab4/pdfminer_six-20260107-py3-none-any.whl", hash = "sha256:366585ba97e80dffa8f00cebe303d2f381884d8637af4ce422f1df3ef38111a9", size = 6592252, upload-time = "2026-01-07T13:29:10.742Z" },
+]
+
+[[package]]
+name = "peft"
+version = "0.18.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "accelerate" },
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "psutil" },
+ { name = "pyyaml" },
+ { name = "safetensors" },
+ { name = "torch" },
+ { name = "tqdm" },
+ { name = "transformers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d8/48/147b3ea999560b40a34fd78724c7777aa9d18409c2250bdcaf9c4f2db7fc/peft-0.18.1.tar.gz", hash = "sha256:2dd0d6bfce936d1850e48aaddbd250941c5c02fc8ef3237cd8fd5aac35e0bae2", size = 635030, upload-time = "2026-01-09T13:08:01.136Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/14/b4e3f574acf349ae6f61f9c000a77f97a3b315b4bb6ad03791e79ae4a568/peft-0.18.1-py3-none-any.whl", hash = "sha256:0bf06847a3551e3019fc58c440cffc9a6b73e6e2962c95b52e224f77bbdb50f1", size = 556960, upload-time = "2026-01-09T13:07:55.865Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "10.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", size = 46555059, upload-time = "2024-07-01T09:48:43.583Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", size = 3509350, upload-time = "2024-07-01T09:46:17.177Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", size = 3374980, upload-time = "2024-07-01T09:46:19.169Z" },
+ { url = "https://files.pythonhosted.org/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", size = 4343799, upload-time = "2024-07-01T09:46:21.883Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", size = 4459973, upload-time = "2024-07-01T09:46:24.321Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", size = 4370054, upload-time = "2024-07-01T09:46:26.825Z" },
+ { url = "https://files.pythonhosted.org/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", size = 4539484, upload-time = "2024-07-01T09:46:29.355Z" },
+ { url = "https://files.pythonhosted.org/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", size = 4477375, upload-time = "2024-07-01T09:46:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", size = 4608773, upload-time = "2024-07-01T09:46:33.73Z" },
+ { url = "https://files.pythonhosted.org/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", size = 2235690, upload-time = "2024-07-01T09:46:36.587Z" },
+ { url = "https://files.pythonhosted.org/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", size = 2554951, upload-time = "2024-07-01T09:46:38.777Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", size = 2243427, upload-time = "2024-07-01T09:46:43.15Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", size = 3525685, upload-time = "2024-07-01T09:46:45.194Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", size = 3374883, upload-time = "2024-07-01T09:46:47.331Z" },
+ { url = "https://files.pythonhosted.org/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", size = 4339837, upload-time = "2024-07-01T09:46:49.647Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", size = 4455562, upload-time = "2024-07-01T09:46:51.811Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", size = 4366761, upload-time = "2024-07-01T09:46:53.961Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", size = 4536767, upload-time = "2024-07-01T09:46:56.664Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", size = 4477989, upload-time = "2024-07-01T09:46:58.977Z" },
+ { url = "https://files.pythonhosted.org/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", size = 4610255, upload-time = "2024-07-01T09:47:01.189Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", size = 2235603, upload-time = "2024-07-01T09:47:03.918Z" },
+ { url = "https://files.pythonhosted.org/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", size = 2554972, upload-time = "2024-07-01T09:47:06.152Z" },
+ { url = "https://files.pythonhosted.org/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", size = 2243375, upload-time = "2024-07-01T09:47:09.065Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
+]
+
+[[package]]
+name = "portalocker"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pywin32", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" },
+]
+
+[[package]]
+name = "preshed"
+version = "3.0.12"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cymem" },
+ { name = "murmurhash" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bf/34/eb4f5f0f678e152a96e826da867d2f41c4b18a2d589e40e1dd3347219e91/preshed-3.0.12.tar.gz", hash = "sha256:b73f9a8b54ee1d44529cc6018356896cff93d48f755f29c134734d9371c0d685", size = 15027, upload-time = "2025-11-17T13:00:33.621Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4b/f7/ff3aca937eeaee19c52c45ddf92979546e52ed0686e58be4bc09c47e7d88/preshed-3.0.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2779861f5d69480493519ed123a622a13012d1182126779036b99d9d989bf7e9", size = 129958, upload-time = "2025-11-17T12:59:33.391Z" },
+ { url = "https://files.pythonhosted.org/packages/80/24/fd654a9c0f5f3ed1a9b1d8a392f063ae9ca29ad0b462f0732ae0147f7cee/preshed-3.0.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffe1fd7d92f51ed34383e20d8b734780c814ca869cfdb7e07f2d31651f90cdf4", size = 124550, upload-time = "2025-11-17T12:59:34.688Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/8271c7f680696f4b0880f44357d2a903d649cb9f6e60a1efc97a203104df/preshed-3.0.12-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:91893404858502cc4e856d338fef3d2a4a552135f79a1041c24eb919817c19db", size = 874987, upload-time = "2025-11-17T12:59:36.062Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/a5/ca200187ca1632f1e2c458b72f1bd100fa8b55deecd5d72e1e4ebf09e98c/preshed-3.0.12-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9e06e8f2ba52f183eb9817a616cdebe84a211bb859a2ffbc23f3295d0b189638", size = 866499, upload-time = "2025-11-17T12:59:37.586Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a1/943b61f850c44899910c21996cb542d0ef5931744c6d492fdfdd8457e693/preshed-3.0.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbe8b8a2d4f9af14e8a39ecca524b9de6defc91d8abcc95eb28f42da1c23272c", size = 878064, upload-time = "2025-11-17T12:59:39.651Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/75/d7fff7f1fa3763619aa85d6ba70493a5d9c6e6ea7958a6e8c9d3e6e88bbe/preshed-3.0.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5d0aaac9c5862f5471fddd0c931dc64d3af2efc5fe3eb48b50765adb571243b9", size = 900540, upload-time = "2025-11-17T12:59:41.384Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/12/a2285b78bd097a1e53fb90a1743bc8ce0d35e5b65b6853f3b3c47da398ca/preshed-3.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:0eb8d411afcb1e3b12a0602fb6a0e33140342a732a795251a0ce452aba401dc0", size = 118298, upload-time = "2025-11-17T12:59:42.65Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/34/4e8443fe99206a2fcfc63659969a8f8c8ab184836533594a519f3899b1ad/preshed-3.0.12-cp312-cp312-win_arm64.whl", hash = "sha256:dcd3d12903c9f720a39a5c5f1339f7f46e3ab71279fb7a39776768fb840b6077", size = 104746, upload-time = "2025-11-17T12:59:43.934Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/36/1d3df6f9f37efc34be4ee3013b3bb698b06f1e372f80959851b54d8efdb2/preshed-3.0.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3deb3ab93d50c785eaa7694a8e169eb12d00263a99c91d56511fe943bcbacfb6", size = 128023, upload-time = "2025-11-17T12:59:45.157Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d4/3ca81f42978da1b81aa57b3e9b5193d8093e187787a3b2511d16b30b7c62/preshed-3.0.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604350001238dab63dc14774ee30c257b5d71c7be976dbecd1f1ed37529f60f", size = 122851, upload-time = "2025-11-17T12:59:46.439Z" },
+ { url = "https://files.pythonhosted.org/packages/17/73/f388398f8d789f69b510272d144a9186d658423f6d3ecc484c0fe392acec/preshed-3.0.12-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04fb860a8aab18d2201f06159337eda5568dc5eed218570d960fad79e783c7d0", size = 835926, upload-time = "2025-11-17T12:59:47.882Z" },
+ { url = "https://files.pythonhosted.org/packages/35/c6/b7170933451cbc27eaefd57b36f61a5e7e7c8da50ae24f819172e0ca8a4d/preshed-3.0.12-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d0c8fcd44996031c46a0aa6773c7b7aa5ee58c3ee87bc05236dacd5599d35063", size = 827294, upload-time = "2025-11-17T12:59:49.365Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/ec/6504730d811c0a375721db2107d31684ec17ee5b7bb3796ecfa41e704d41/preshed-3.0.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b07efc3abd3714ce01cf67db0a2dada6e829ab7def74039d446e49ddb32538c5", size = 838809, upload-time = "2025-11-17T12:59:51.234Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/1a/09d13240c1fbadcc0603e2fe029623045a36c88b4b50b02e7fdc89e3b88e/preshed-3.0.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f184ef184b76e0e4707bce2395008779e4dfa638456b13b18469c2c1a42903a6", size = 861448, upload-time = "2025-11-17T12:59:52.702Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/35/9523160153037ee8337672249449be416ee92236f32602e7dd643767814f/preshed-3.0.12-cp313-cp313-win_amd64.whl", hash = "sha256:ebb3da2dc62ab09e5dc5a00ec38e7f5cdf8741c175714ab4a80773d8ee31b495", size = 117413, upload-time = "2025-11-17T12:59:54.4Z" },
+ { url = "https://files.pythonhosted.org/packages/79/eb/4263e6e896753b8e2ffa93035458165850a5ea81d27e8888afdbfd8fa9c4/preshed-3.0.12-cp313-cp313-win_arm64.whl", hash = "sha256:b36a2cf57a5ca6e78e69b569c92ef3bdbfb00e3a14859e201eec6ab3bdc27085", size = 104041, upload-time = "2025-11-17T12:59:55.596Z" },
+ { url = "https://files.pythonhosted.org/packages/77/39/7b33910b7ba3db9ce1515c39eb4657232913fb171fe701f792ef50726e60/preshed-3.0.12-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0d8b458dfbd6cc5007d045fa5638231328e3d6f214fd24ab999cc10f8b9097e5", size = 129211, upload-time = "2025-11-17T12:59:57.182Z" },
+ { url = "https://files.pythonhosted.org/packages/32/67/97dceebe0b2b4dd94333e4ec283d38614f92996de615859a952da082890d/preshed-3.0.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8e9196e2ea704243a69df203e0c9185eb7c5c58c3632ba1c1e2e2e0aa3aae3b4", size = 123311, upload-time = "2025-11-17T12:59:58.449Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/6f/f3772f6eaad1eae787f82ffb65a81a4a1993277eacf5a78a29da34608323/preshed-3.0.12-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ffa644e1730012ed435fb9d0c3031ea19a06b11136eff5e9b96b2aa25ec7a5f5", size = 831683, upload-time = "2025-11-17T13:00:00.229Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/93/997d39ca61202486dd06c669b4707a5b8e5d0c2c922db9f7744fd6a12096/preshed-3.0.12-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:39e83a16ce53e4a3c41c091fe4fe1c3d28604e63928040da09ba0c5d5a7ca41e", size = 830035, upload-time = "2025-11-17T13:00:02.191Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/f2/51bf44e3fdbef08d40a832181842cd9b21b11c3f930989f4ff17e9201e12/preshed-3.0.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2ec9bc0baee426303a644c7bf531333d4e7fd06fedf07f62ee09969c208d578d", size = 841728, upload-time = "2025-11-17T13:00:03.643Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/b1/2d0e3d23d9f885f7647654d770227eb13e4d892deb9b0ed50b993d63fb18/preshed-3.0.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7db058f1b4a3d4d51c4c05b379c6cc9c36fcad00160923cb20ca1c7030581ea4", size = 858860, upload-time = "2025-11-17T13:00:05.185Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/57/7c28c7f6f9bfce02796b54f1f6acd2cebb3fa3f14a2dce6fb3c686e3c3a8/preshed-3.0.12-cp314-cp314-win_amd64.whl", hash = "sha256:c87a54a55a2ba98d0c3fd7886295f2825397aff5a7157dcfb89124f6aa2dca41", size = 120325, upload-time = "2025-11-17T13:00:06.428Z" },
+ { url = "https://files.pythonhosted.org/packages/33/c3/df235ca679a08e09103983ec17c668f96abe897eadbe18d635972b43d8a9/preshed-3.0.12-cp314-cp314-win_arm64.whl", hash = "sha256:d9c5f10b4b971d71d163c2416b91b7136eae54ef3183b1742bb5993269af1b18", size = 107393, upload-time = "2025-11-17T13:00:07.718Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/f1/51a2a72381c8aa3aeb8305d88e720c745048527107e649c01b8d49d6b5bf/preshed-3.0.12-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2739a9c57efcfa16466fa6e0257d67f0075a9979dc729585fbadaed7383ab449", size = 137703, upload-time = "2025-11-17T13:00:09.001Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ab/f3c3d50647f3af6ce6441c596a4f6fb0216d549432ef51f61c0c1744c9b9/preshed-3.0.12-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:364249656bfbf98b4008fac707f35835580ec56207f7cbecdafef6ebb6a595a6", size = 134889, upload-time = "2025-11-17T13:00:10.29Z" },
+ { url = "https://files.pythonhosted.org/packages/54/9a/012dbae28a0b88cd98eae99f87701ffbe3a7d2ea3de345cb8a6a6e1b16cd/preshed-3.0.12-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7f933d509ee762a90f62573aaf189eba94dfee478fca13ea2183b2f8a1bb8f7e", size = 911078, upload-time = "2025-11-17T13:00:11.911Z" },
+ { url = "https://files.pythonhosted.org/packages/88/c1/0cd0f8cdb91f63c298320cf946c4b97adfb8e8d3a5d454267410c90fcfaa/preshed-3.0.12-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f73f4e29bf90e58034e6f5fa55e6029f3f2d7c042a7151ed487b49898b0ce887", size = 930506, upload-time = "2025-11-17T13:00:13.375Z" },
+ { url = "https://files.pythonhosted.org/packages/20/1a/cab79b3181b2150eeeb0e2541c2bd4e0830e1e068b8836b24ea23610cec3/preshed-3.0.12-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a61ede0c3d18f1ae128113f785a396351a46f4634beccfdf617b0a86008b154d", size = 900009, upload-time = "2025-11-17T13:00:14.781Z" },
+ { url = "https://files.pythonhosted.org/packages/31/9a/5ea9d6d95d5c07ba70166330a43bff7f0a074d0134eb7984eca6551e8c70/preshed-3.0.12-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eafc08a86f77be78e722d96aa8a3a0aef0e3c7ac2f2ada22186a138e63d4033c", size = 910826, upload-time = "2025-11-17T13:00:16.861Z" },
+ { url = "https://files.pythonhosted.org/packages/92/71/39024f9873ff317eac724b2759e94d013703800d970d51de77ccc6afff7e/preshed-3.0.12-cp314-cp314t-win_amd64.whl", hash = "sha256:fadaad54973b8697d5ef008735e150bd729a127b6497fd2cb068842074a6f3a7", size = 141358, upload-time = "2025-11-17T13:00:18.167Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/0d/431bb85252119f5d2260417fa7d164619b31eed8f1725b364dc0ade43a8e/preshed-3.0.12-cp314-cp314t-win_arm64.whl", hash = "sha256:c0c0d3b66b4c1e40aa6042721492f7b07fc9679ab6c361bc121aa54a1c3ef63f", size = 114839, upload-time = "2025-11-17T13:00:19.513Z" },
+]
+
+[[package]]
+name = "propcache"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" },
+ { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" },
+ { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" },
+ { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" },
+ { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" },
+ { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" },
+ { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" },
+ { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" },
+ { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" },
+ { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" },
+ { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" },
+ { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" },
+ { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" },
+ { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" },
+ { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" },
+ { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" },
+ { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" },
+ { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" },
+ { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" },
+ { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" },
+ { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" },
+ { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" },
+ { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" },
+ { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" },
+ { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" },
+ { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" },
+ { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" },
+ { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" },
+ { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" },
+ { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" },
+ { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" },
+]
+
+[[package]]
+name = "proto-plus"
+version = "1.27.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "protobuf" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/01/89/9cbe2f4bba860e149108b683bc2efec21f14d5f7ed6e25562ad86acbc373/proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4", size = 56158, upload-time = "2025-12-16T13:46:25.729Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", size = 50205, upload-time = "2025-12-16T13:46:24.76Z" },
+]
+
+[[package]]
+name = "protobuf"
+version = "6.33.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/53/b8/cda15d9d46d03d4aa3a67cb6bffe05173440ccf86a9541afaf7ac59a1b6b/protobuf-6.33.4.tar.gz", hash = "sha256:dc2e61bca3b10470c1912d166fe0af67bfc20eb55971dcef8dfa48ce14f0ed91", size = 444346, upload-time = "2026-01-12T18:33:40.109Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/be/24ef9f3095bacdf95b458543334d0c4908ccdaee5130420bf064492c325f/protobuf-6.33.4-cp310-abi3-win32.whl", hash = "sha256:918966612c8232fc6c24c78e1cd89784307f5814ad7506c308ee3cf86662850d", size = 425612, upload-time = "2026-01-12T18:33:29.656Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ad/e5693e1974a28869e7cd244302911955c1cebc0161eb32dfa2b25b6e96f0/protobuf-6.33.4-cp310-abi3-win_amd64.whl", hash = "sha256:8f11ffae31ec67fc2554c2ef891dcb561dae9a2a3ed941f9e134c2db06657dbc", size = 436962, upload-time = "2026-01-12T18:33:31.345Z" },
+ { url = "https://files.pythonhosted.org/packages/66/15/6ee23553b6bfd82670207ead921f4d8ef14c107e5e11443b04caeb5ab5ec/protobuf-6.33.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2fe67f6c014c84f655ee06f6f66213f9254b3a8b6bda6cda0ccd4232c73c06f0", size = 427612, upload-time = "2026-01-12T18:33:32.646Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/48/d301907ce6d0db75f959ca74f44b475a9caa8fcba102d098d3c3dd0f2d3f/protobuf-6.33.4-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:757c978f82e74d75cba88eddec479df9b99a42b31193313b75e492c06a51764e", size = 324484, upload-time = "2026-01-12T18:33:33.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/1c/e53078d3f7fe710572ab2dcffd993e1e3b438ae71cfc031b71bae44fcb2d/protobuf-6.33.4-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c7c64f259c618f0bef7bee042075e390debbf9682334be2b67408ec7c1c09ee6", size = 339256, upload-time = "2026-01-12T18:33:35.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/8e/971c0edd084914f7ee7c23aa70ba89e8903918adca179319ee94403701d5/protobuf-6.33.4-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:3df850c2f8db9934de4cf8f9152f8dc2558f49f298f37f90c517e8e5c84c30e9", size = 323311, upload-time = "2026-01-12T18:33:36.305Z" },
+ { url = "https://files.pythonhosted.org/packages/75/b1/1dc83c2c661b4c62d56cc081706ee33a4fc2835bd90f965baa2663ef7676/protobuf-6.33.4-py3-none-any.whl", hash = "sha256:1fe3730068fcf2e595816a6c34fe66eeedd37d51d0400b72fabc848811fdc1bc", size = 170532, upload-time = "2026-01-12T18:33:39.199Z" },
+]
+
+[[package]]
+name = "psutil"
+version = "7.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/73/cb/09e5184fb5fc0358d110fc3ca7f6b1d033800734d34cac10f4136cfac10e/psutil-7.2.1.tar.gz", hash = "sha256:f7583aec590485b43ca601dd9cea0dcd65bd7bb21d30ef4ddbf4ea6b5ed1bdd3", size = 490253, upload-time = "2025-12-29T08:26:00.169Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/77/8e/f0c242053a368c2aa89584ecd1b054a18683f13d6e5a318fc9ec36582c94/psutil-7.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ba9f33bb525b14c3ea563b2fd521a84d2fa214ec59e3e6a2858f78d0844dd60d", size = 129624, upload-time = "2025-12-29T08:26:04.255Z" },
+ { url = "https://files.pythonhosted.org/packages/26/97/a58a4968f8990617decee234258a2b4fc7cd9e35668387646c1963e69f26/psutil-7.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:81442dac7abfc2f4f4385ea9e12ddf5a796721c0f6133260687fec5c3780fa49", size = 130132, upload-time = "2025-12-29T08:26:06.228Z" },
+ { url = "https://files.pythonhosted.org/packages/db/6d/ed44901e830739af5f72a85fa7ec5ff1edea7f81bfbf4875e409007149bd/psutil-7.2.1-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea46c0d060491051d39f0d2cff4f98d5c72b288289f57a21556cc7d504db37fc", size = 180612, upload-time = "2025-12-29T08:26:08.276Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/65/b628f8459bca4efbfae50d4bf3feaab803de9a160b9d5f3bd9295a33f0c2/psutil-7.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:35630d5af80d5d0d49cfc4d64c1c13838baf6717a13effb35869a5919b854cdf", size = 183201, upload-time = "2025-12-29T08:26:10.622Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/23/851cadc9764edcc18f0effe7d0bf69f727d4cf2442deb4a9f78d4e4f30f2/psutil-7.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:923f8653416604e356073e6e0bccbe7c09990acef442def2f5640dd0faa9689f", size = 139081, upload-time = "2025-12-29T08:26:12.483Z" },
+ { url = "https://files.pythonhosted.org/packages/59/82/d63e8494ec5758029f31c6cb06d7d161175d8281e91d011a4a441c8a43b5/psutil-7.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cfbe6b40ca48019a51827f20d830887b3107a74a79b01ceb8cc8de4ccb17b672", size = 134767, upload-time = "2025-12-29T08:26:14.528Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c2/5fb764bd61e40e1fe756a44bd4c21827228394c17414ade348e28f83cd79/psutil-7.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:494c513ccc53225ae23eec7fe6e1482f1b8a44674241b54561f755a898650679", size = 129716, upload-time = "2025-12-29T08:26:16.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d2/935039c20e06f615d9ca6ca0ab756cf8408a19d298ffaa08666bc18dc805/psutil-7.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fce5f92c22b00cdefd1645aa58ab4877a01679e901555067b1bd77039aa589f", size = 130133, upload-time = "2025-12-29T08:26:18.009Z" },
+ { url = "https://files.pythonhosted.org/packages/77/69/19f1eb0e01d24c2b3eacbc2f78d3b5add8a89bf0bb69465bc8d563cc33de/psutil-7.2.1-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93f3f7b0bb07711b49626e7940d6fe52aa9940ad86e8f7e74842e73189712129", size = 181518, upload-time = "2025-12-29T08:26:20.241Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/6d/7e18b1b4fa13ad370787626c95887b027656ad4829c156bb6569d02f3262/psutil-7.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d34d2ca888208eea2b5c68186841336a7f5e0b990edec929be909353a202768a", size = 184348, upload-time = "2025-12-29T08:26:22.215Z" },
+ { url = "https://files.pythonhosted.org/packages/98/60/1672114392dd879586d60dd97896325df47d9a130ac7401318005aab28ec/psutil-7.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2ceae842a78d1603753561132d5ad1b2f8a7979cb0c283f5b52fb4e6e14b1a79", size = 140400, upload-time = "2025-12-29T08:26:23.993Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/7b/d0e9d4513c46e46897b46bcfc410d51fc65735837ea57a25170f298326e6/psutil-7.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:08a2f175e48a898c8eb8eace45ce01777f4785bc744c90aa2cc7f2fa5462a266", size = 135430, upload-time = "2025-12-29T08:26:25.999Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/cf/5180eb8c8bdf6a503c6919f1da28328bd1e6b3b1b5b9d5b01ae64f019616/psutil-7.2.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b2e953fcfaedcfbc952b44744f22d16575d3aa78eb4f51ae74165b4e96e55f42", size = 128137, upload-time = "2025-12-29T08:26:27.759Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/2c/78e4a789306a92ade5000da4f5de3255202c534acdadc3aac7b5458fadef/psutil-7.2.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:05cc68dbb8c174828624062e73078e7e35406f4ca2d0866c272c2410d8ef06d1", size = 128947, upload-time = "2025-12-29T08:26:29.548Z" },
+ { url = "https://files.pythonhosted.org/packages/29/f8/40e01c350ad9a2b3cb4e6adbcc8a83b17ee50dd5792102b6142385937db5/psutil-7.2.1-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e38404ca2bb30ed7267a46c02f06ff842e92da3bb8c5bfdadbd35a5722314d8", size = 154694, upload-time = "2025-12-29T08:26:32.147Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e4/b751cdf839c011a9714a783f120e6a86b7494eb70044d7d81a25a5cd295f/psutil-7.2.1-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab2b98c9fc19f13f59628d94df5cc4cc4844bc572467d113a8b517d634e362c6", size = 156136, upload-time = "2025-12-29T08:26:34.079Z" },
+ { url = "https://files.pythonhosted.org/packages/44/ad/bbf6595a8134ee1e94a4487af3f132cef7fce43aef4a93b49912a48c3af7/psutil-7.2.1-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f78baafb38436d5a128f837fab2d92c276dfb48af01a240b861ae02b2413ada8", size = 148108, upload-time = "2025-12-29T08:26:36.225Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/15/dd6fd869753ce82ff64dcbc18356093471a5a5adf4f77ed1f805d473d859/psutil-7.2.1-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:99a4cd17a5fdd1f3d014396502daa70b5ec21bf4ffe38393e152f8e449757d67", size = 147402, upload-time = "2025-12-29T08:26:39.21Z" },
+ { url = "https://files.pythonhosted.org/packages/34/68/d9317542e3f2b180c4306e3f45d3c922d7e86d8ce39f941bb9e2e9d8599e/psutil-7.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:b1b0671619343aa71c20ff9767eced0483e4fc9e1f489d50923738caf6a03c17", size = 136938, upload-time = "2025-12-29T08:26:41.036Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/73/2ce007f4198c80fcf2cb24c169884f833fe93fbc03d55d302627b094ee91/psutil-7.2.1-cp37-abi3-win_arm64.whl", hash = "sha256:0d67c1822c355aa6f7314d92018fb4268a76668a536f133599b91edd48759442", size = 133836, upload-time = "2025-12-29T08:26:43.086Z" },
+]
+
+[[package]]
+name = "pyarrow"
+version = "23.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/33/ffd9c3eb087fa41dd79c3cf20c4c0ae3cdb877c4f8e1107a446006344924/pyarrow-23.0.0.tar.gz", hash = "sha256:180e3150e7edfcd182d3d9afba72f7cf19839a497cc76555a8dce998a8f67615", size = 1167185, upload-time = "2026-01-18T16:19:42.218Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/bd/c861d020831ee57609b73ea721a617985ece817684dc82415b0bc3e03ac3/pyarrow-23.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5961a9f646c232697c24f54d3419e69b4261ba8a8b66b0ac54a1851faffcbab8", size = 34189116, upload-time = "2026-01-18T16:15:28.054Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/23/7725ad6cdcbaf6346221391e7b3eecd113684c805b0a95f32014e6fa0736/pyarrow-23.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:632b3e7c3d232f41d64e1a4a043fb82d44f8a349f339a1188c6a0dd9d2d47d8a", size = 35803831, upload-time = "2026-01-18T16:15:33.798Z" },
+ { url = "https://files.pythonhosted.org/packages/57/06/684a421543455cdc2944d6a0c2cc3425b028a4c6b90e34b35580c4899743/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:76242c846db1411f1d6c2cc3823be6b86b40567ee24493344f8226ba34a81333", size = 44436452, upload-time = "2026-01-18T16:15:41.598Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/6f/8f9eb40c2328d66e8b097777ddcf38494115ff9f1b5bc9754ba46991191e/pyarrow-23.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b73519f8b52ae28127000986bf228fda781e81d3095cd2d3ece76eb5cf760e1b", size = 47557396, upload-time = "2026-01-18T16:15:51.252Z" },
+ { url = "https://files.pythonhosted.org/packages/10/6e/f08075f1472e5159553501fde2cc7bc6700944bdabe49a03f8a035ee6ccd/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:068701f6823449b1b6469120f399a1239766b117d211c5d2519d4ed5861f75de", size = 48147129, upload-time = "2026-01-18T16:16:00.299Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/82/d5a680cd507deed62d141cc7f07f7944a6766fc51019f7f118e4d8ad0fb8/pyarrow-23.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1801ba947015d10e23bca9dd6ef5d0e9064a81569a89b6e9a63b59224fd060df", size = 50596642, upload-time = "2026-01-18T16:16:08.502Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/26/4f29c61b3dce9fa7780303b86895ec6a0917c9af927101daaaf118fbe462/pyarrow-23.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:52265266201ec25b6839bf6bd4ea918ca6d50f31d13e1cf200b4261cd11dc25c", size = 27660628, upload-time = "2026-01-18T16:16:15.28Z" },
+ { url = "https://files.pythonhosted.org/packages/66/34/564db447d083ec7ff93e0a883a597d2f214e552823bfc178a2d0b1f2c257/pyarrow-23.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ad96a597547af7827342ffb3c503c8316e5043bb09b47a84885ce39394c96e00", size = 34184630, upload-time = "2026-01-18T16:16:22.141Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/3a/3999daebcb5e6119690c92a621c4d78eef2ffba7a0a1b56386d2875fcd77/pyarrow-23.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b9edf990df77c2901e79608f08c13fbde60202334a4fcadb15c1f57bf7afee43", size = 35796820, upload-time = "2026-01-18T16:16:29.441Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ee/39195233056c6a8d0976d7d1ac1cd4fe21fb0ec534eca76bc23ef3f60e11/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:36d1b5bc6ddcaff0083ceec7e2561ed61a51f49cce8be079ee8ed406acb6fdef", size = 44438735, upload-time = "2026-01-18T16:16:38.79Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/41/6a7328ee493527e7afc0c88d105ecca69a3580e29f2faaeac29308369fd7/pyarrow-23.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4292b889cd224f403304ddda8b63a36e60f92911f89927ec8d98021845ea21be", size = 47557263, upload-time = "2026-01-18T16:16:46.248Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/ee/34e95b21ee84db494eae60083ddb4383477b31fb1fd19fd866d794881696/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dfd9e133e60eaa847fd80530a1b89a052f09f695d0b9c34c235ea6b2e0924cf7", size = 48153529, upload-time = "2026-01-18T16:16:53.412Z" },
+ { url = "https://files.pythonhosted.org/packages/52/88/8a8d83cea30f4563efa1b7bf51d241331ee5cd1b185a7e063f5634eca415/pyarrow-23.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832141cc09fac6aab1cd3719951d23301396968de87080c57c9a7634e0ecd068", size = 50598851, upload-time = "2026-01-18T16:17:01.133Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/4c/2929c4be88723ba025e7b3453047dc67e491c9422965c141d24bab6b5962/pyarrow-23.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:7a7d067c9a88faca655c71bcc30ee2782038d59c802d57950826a07f60d83c4c", size = 27577747, upload-time = "2026-01-18T16:18:02.413Z" },
+ { url = "https://files.pythonhosted.org/packages/64/52/564a61b0b82d72bd68ec3aef1adda1e3eba776f89134b9ebcb5af4b13cb6/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ce9486e0535a843cf85d990e2ec5820a47918235183a5c7b8b97ed7e92c2d47d", size = 34446038, upload-time = "2026-01-18T16:17:07.861Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/c9/232d4f9855fd1de0067c8a7808a363230d223c83aeee75e0fe6eab851ba9/pyarrow-23.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:075c29aeaa685fd1182992a9ed2499c66f084ee54eea47da3eb76e125e06064c", size = 35921142, upload-time = "2026-01-18T16:17:15.401Z" },
+ { url = "https://files.pythonhosted.org/packages/96/f2/60af606a3748367b906bb82d41f0032e059f075444445d47e32a7ff1df62/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:799965a5379589510d888be3094c2296efd186a17ca1cef5b77703d4d5121f53", size = 44490374, upload-time = "2026-01-18T16:17:23.93Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/2d/7731543050a678ea3a413955a2d5d80d2a642f270aa57a3cb7d5a86e3f46/pyarrow-23.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef7cac8fe6fccd8b9e7617bfac785b0371a7fe26af59463074e4882747145d40", size = 47527896, upload-time = "2026-01-18T16:17:33.393Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/90/f3342553b7ac9879413aed46500f1637296f3c8222107523a43a1c08b42a/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15a414f710dc927132dd67c361f78c194447479555af57317066ee5116b90e9e", size = 48210401, upload-time = "2026-01-18T16:17:42.012Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/da/9862ade205ecc46c172b6ce5038a74b5151c7401e36255f15975a45878b2/pyarrow-23.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3e0d2e6915eca7d786be6a77bf227fbc06d825a75b5b5fe9bcbef121dec32685", size = 50579677, upload-time = "2026-01-18T16:17:50.241Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/4c/f11f371f5d4740a5dafc2e11c76bcf42d03dfdb2d68696da97de420b6963/pyarrow-23.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4b317ea6e800b5704e5e5929acb6e2dc13e9276b708ea97a39eb8b345aa2658b", size = 27631889, upload-time = "2026-01-18T16:17:56.55Z" },
+ { url = "https://files.pythonhosted.org/packages/97/bb/15aec78bcf43a0c004067bd33eb5352836a29a49db8581fc56f2b6ca88b7/pyarrow-23.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:20b187ed9550d233a872074159f765f52f9d92973191cd4b93f293a19efbe377", size = 34213265, upload-time = "2026-01-18T16:18:07.904Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/6c/deb2c594bbba41c37c5d9aa82f510376998352aa69dfcb886cb4b18ad80f/pyarrow-23.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:18ec84e839b493c3886b9b5e06861962ab4adfaeb79b81c76afbd8d84c7d5fda", size = 35819211, upload-time = "2026-01-18T16:18:13.94Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/e5/ee82af693cb7b5b2b74f6524cdfede0e6ace779d7720ebca24d68b57c36b/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:e438dd3f33894e34fd02b26bd12a32d30d006f5852315f611aa4add6c7fab4bc", size = 44502313, upload-time = "2026-01-18T16:18:20.367Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/86/95c61ad82236495f3c31987e85135926ba3ec7f3819296b70a68d8066b49/pyarrow-23.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:a244279f240c81f135631be91146d7fa0e9e840e1dfed2aba8483eba25cd98e6", size = 47585886, upload-time = "2026-01-18T16:18:27.544Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/6e/a72d901f305201802f016d015de1e05def7706fff68a1dedefef5dc7eff7/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c4692e83e42438dba512a570c6eaa42be2f8b6c0f492aea27dec54bdc495103a", size = 48207055, upload-time = "2026-01-18T16:18:35.425Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e5/5de029c537630ca18828db45c30e2a78da03675a70ac6c3528203c416fe3/pyarrow-23.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae7f30f898dfe44ea69654a35c93e8da4cef6606dc4c72394068fd95f8e9f54a", size = 50619812, upload-time = "2026-01-18T16:18:43.553Z" },
+ { url = "https://files.pythonhosted.org/packages/59/8d/2af846cd2412e67a087f5bda4a8e23dfd4ebd570f777db2e8686615dafc1/pyarrow-23.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:5b86bb649e4112fb0614294b7d0a175c7513738876b89655605ebb87c804f861", size = 28263851, upload-time = "2026-01-18T16:19:38.567Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/7f/caab863e587041156f6786c52e64151b7386742c8c27140f637176e9230e/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:ebc017d765d71d80a3f8584ca0566b53e40464586585ac64176115baa0ada7d3", size = 34463240, upload-time = "2026-01-18T16:18:49.755Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/fa/3a5b8c86c958e83622b40865e11af0857c48ec763c11d472c87cd518283d/pyarrow-23.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:0800cc58a6d17d159df823f87ad66cefebf105b982493d4bad03ee7fab84b993", size = 35935712, upload-time = "2026-01-18T16:18:55.626Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/08/17a62078fc1a53decb34a9aa79cf9009efc74d63d2422e5ade9fed2f99e3/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3a7c68c722da9bb5b0f8c10e3eae71d9825a4b429b40b32709df5d1fa55beb3d", size = 44503523, upload-time = "2026-01-18T16:19:03.958Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/70/84d45c74341e798aae0323d33b7c39194e23b1abc439ceaf60a68a7a969a/pyarrow-23.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:bd5556c24622df90551063ea41f559b714aa63ca953db884cfb958559087a14e", size = 47542490, upload-time = "2026-01-18T16:19:11.208Z" },
+ { url = "https://files.pythonhosted.org/packages/61/d9/d1274b0e6f19e235de17441e53224f4716574b2ca837022d55702f24d71d/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54810f6e6afc4ffee7c2e0051b61722fbea9a4961b46192dcfae8ea12fa09059", size = 48233605, upload-time = "2026-01-18T16:19:19.544Z" },
+ { url = "https://files.pythonhosted.org/packages/39/07/e4e2d568cb57543d84482f61e510732820cddb0f47c4bb7df629abfed852/pyarrow-23.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:14de7d48052cf4b0ed174533eafa3cfe0711b8076ad70bede32cf59f744f0d7c", size = 50603979, upload-time = "2026-01-18T16:19:26.717Z" },
+ { url = "https://files.pythonhosted.org/packages/72/9c/47693463894b610f8439b2e970b82ef81e9599c757bf2049365e40ff963c/pyarrow-23.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:427deac1f535830a744a4f04a6ac183a64fcac4341b3f618e693c41b7b98d2b0", size = 28338905, upload-time = "2026-01-18T16:19:32.93Z" },
+]
+
+[[package]]
+name = "pyasn1"
+version = "0.6.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
+]
+
+[[package]]
+name = "pyasn1-modules"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.12.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-types" },
+ { name = "pydantic-core" },
+ { name = "typing-extensions" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
+]
+
+[package.optional-dependencies]
+email = [
+ { name = "email-validator" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.41.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
+ { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
+ { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
+ { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
+ { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
+ { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
+ { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
+ { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
+ { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
+ { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
+ { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
+ { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
+ { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" },
+ { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" },
+ { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" },
+ { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" },
+ { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
+ { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
+]
+
+[[package]]
+name = "pydantic-settings"
+version = "2.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pydantic" },
+ { name = "python-dotenv" },
+ { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
+]
+
+[[package]]
+name = "pydub"
+version = "0.25.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.3.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
+]
+
+[[package]]
+name = "pypdf"
+version = "6.6.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b8/bb/a44bab1ac3c54dbcf653d7b8bcdee93dddb2d3bf025a3912cacb8149a2f2/pypdf-6.6.2.tar.gz", hash = "sha256:0a3ea3b3303982333404e22d8f75d7b3144f9cf4b2970b96856391a516f9f016", size = 5281850, upload-time = "2026-01-26T11:57:55.964Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7d/be/549aaf1dfa4ab4aed29b09703d2fb02c4366fc1f05e880948c296c5764b9/pypdf-6.6.2-py3-none-any.whl", hash = "sha256:44c0c9811cfb3b83b28f1c3d054531d5b8b81abaedee0d8cb403650d023832ba", size = 329132, upload-time = "2026-01-26T11:57:54.099Z" },
+]
+
+[[package]]
+name = "pyperclip"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/52/d87eba7cb129b81563019d1679026e7a112ef76855d6159d24754dbd2a51/pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6", size = 12185, upload-time = "2025-09-26T14:40:37.245Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
+]
+
+[[package]]
+name = "pyreadline3"
+version = "3.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839, upload-time = "2024-09-19T02:40:10.062Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+]
+
+[[package]]
+name = "python-multipart"
+version = "0.0.22"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+]
+
+[[package]]
+name = "pytz"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
+]
+
+[[package]]
+name = "pywin32"
+version = "311"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" },
+ { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" },
+]
+
+[[package]]
+name = "pyyaml"
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "qdrant-client"
+version = "1.16.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "grpcio" },
+ { name = "httpx", extra = ["http2"] },
+ { name = "numpy" },
+ { name = "portalocker" },
+ { name = "protobuf" },
+ { name = "pydantic" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ca/7d/3cd10e26ae97b35cf856ca1dc67576e42414ae39502c51165bb36bb1dff8/qdrant_client-1.16.2.tar.gz", hash = "sha256:ca4ef5f9be7b5eadeec89a085d96d5c723585a391eb8b2be8192919ab63185f0", size = 331112, upload-time = "2025-12-12T10:58:30.866Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/13/8ce16f808297e16968269de44a14f4fef19b64d9766be1d6ba5ba78b579d/qdrant_client-1.16.2-py3-none-any.whl", hash = "sha256:442c7ef32ae0f005e88b5d3c0783c63d4912b97ae756eb5e052523be682f17d3", size = 377186, upload-time = "2025-12-12T10:58:29.282Z" },
+]
+
+[[package]]
+name = "referencing"
+version = "0.36.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "attrs" },
+ { name = "rpds-py" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
+]
+
+[[package]]
+name = "regex"
+version = "2026.1.15"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" },
+ { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" },
+ { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" },
+ { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" },
+ { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" },
+ { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" },
+ { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" },
+ { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" },
+ { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" },
+ { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" },
+ { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" },
+ { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" },
+ { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" },
+ { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" },
+ { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" },
+ { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" },
+ { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" },
+ { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" },
+ { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" },
+ { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" },
+ { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" },
+ { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" },
+ { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" },
+ { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" },
+ { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" },
+ { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" },
+ { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" },
+ { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" },
+ { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" },
+ { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" },
+ { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" },
+]
+
+[[package]]
+name = "requests"
+version = "2.32.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+]
+
+[[package]]
+name = "rfc3339-validator"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" },
+]
+
+[[package]]
+name = "rich"
+version = "14.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a1/84/4831f881aa6ff3c976f6d6809b58cdfa350593ffc0dc3c58f5f6586780fb/rich-14.3.1.tar.gz", hash = "sha256:b8c5f568a3a749f9290ec6bddedf835cec33696bfc1e48bcfecb276c7386e4b8", size = 230125, upload-time = "2026-01-24T21:40:44.847Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/87/2a/a1810c8627b9ec8c57ec5ec325d306701ae7be50235e8fd81266e002a3cc/rich-14.3.1-py3-none-any.whl", hash = "sha256:da750b1aebbff0b372557426fb3f35ba56de8ef954b3190315eb64076d6fb54e", size = 309952, upload-time = "2026-01-24T21:40:42.969Z" },
+]
+
+[[package]]
+name = "rich-rst"
+version = "1.3.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "docutils" },
+ { name = "rich" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" },
+]
+
+[[package]]
+name = "rpds-py"
+version = "0.30.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+]
+
+[[package]]
+name = "rsa"
+version = "4.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.14.14"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
+ { url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
+ { url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
+ { url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
+ { url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
+]
+
+[[package]]
+name = "safetensors"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" },
+ { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" },
+ { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" },
+ { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" },
+ { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" },
+]
+
+[[package]]
+name = "scikit-learn"
+version = "1.8.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "joblib" },
+ { name = "numpy" },
+ { name = "scipy" },
+ { name = "threadpoolctl" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" },
+ { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" },
+ { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" },
+ { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" },
+ { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" },
+ { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" },
+ { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" },
+ { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" },
+ { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" },
+ { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" },
+ { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" },
+ { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" },
+ { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" },
+ { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" },
+ { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" },
+]
+
+[[package]]
+name = "scipy"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/56/3e/9cca699f3486ce6bc12ff46dc2031f1ec8eb9ccc9a320fdaf925f1417426/scipy-1.17.0.tar.gz", hash = "sha256:2591060c8e648d8b96439e111ac41fd8342fdeff1876be2e19dea3fe8930454e", size = 30396830, upload-time = "2026-01-10T21:34:23.009Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0b/11/7241a63e73ba5a516f1930ac8d5b44cbbfabd35ac73a2d08ca206df007c4/scipy-1.17.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:0d5018a57c24cb1dd828bcf51d7b10e65986d549f52ef5adb6b4d1ded3e32a57", size = 31364580, upload-time = "2026-01-10T21:25:25.717Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1d/5057f812d4f6adc91a20a2d6f2ebcdb517fdbc87ae3acc5633c9b97c8ba5/scipy-1.17.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:88c22af9e5d5a4f9e027e26772cc7b5922fab8bcc839edb3ae33de404feebd9e", size = 27969012, upload-time = "2026-01-10T21:25:30.921Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/21/f6ec556c1e3b6ec4e088da667d9987bb77cc3ab3026511f427dc8451187d/scipy-1.17.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f3cd947f20fe17013d401b64e857c6b2da83cae567adbb75b9dcba865abc66d8", size = 20140691, upload-time = "2026-01-10T21:25:34.802Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/fe/5e5ad04784964ba964a96f16c8d4676aa1b51357199014dce58ab7ec5670/scipy-1.17.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e8c0b331c2c1f531eb51f1b4fc9ba709521a712cce58f1aa627bc007421a5306", size = 22463015, upload-time = "2026-01-10T21:25:39.277Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/69/7c347e857224fcaf32a34a05183b9d8a7aca25f8f2d10b8a698b8388561a/scipy-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5194c445d0a1c7a6c1a4a4681b6b7c71baad98ff66d96b949097e7513c9d6742", size = 32724197, upload-time = "2026-01-10T21:25:44.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/fe/66d73b76d378ba8cc2fe605920c0c75092e3a65ae746e1e767d9d020a75a/scipy-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9eeb9b5f5997f75507814ed9d298ab23f62cf79f5a3ef90031b1ee2506abdb5b", size = 35009148, upload-time = "2026-01-10T21:25:50.591Z" },
+ { url = "https://files.pythonhosted.org/packages/af/07/07dec27d9dc41c18d8c43c69e9e413431d20c53a0339c388bcf72f353c4b/scipy-1.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:40052543f7bbe921df4408f46003d6f01c6af109b9e2c8a66dd1cf6cf57f7d5d", size = 34798766, upload-time = "2026-01-10T21:25:59.41Z" },
+ { url = "https://files.pythonhosted.org/packages/81/61/0470810c8a093cdacd4ba7504b8a218fd49ca070d79eca23a615f5d9a0b0/scipy-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0cf46c8013fec9d3694dc572f0b54100c28405d55d3e2cb15e2895b25057996e", size = 37405953, upload-time = "2026-01-10T21:26:07.75Z" },
+ { url = "https://files.pythonhosted.org/packages/92/ce/672ed546f96d5d41ae78c4b9b02006cedd0b3d6f2bf5bb76ea455c320c28/scipy-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:0937a0b0d8d593a198cededd4c439a0ea216a3f36653901ea1f3e4be949056f8", size = 36328121, upload-time = "2026-01-10T21:26:16.509Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/21/38165845392cae67b61843a52c6455d47d0cc2a40dd495c89f4362944654/scipy-1.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:f603d8a5518c7426414d1d8f82e253e454471de682ce5e39c29adb0df1efb86b", size = 24314368, upload-time = "2026-01-10T21:26:23.087Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/51/3468fdfd49387ddefee1636f5cf6d03ce603b75205bf439bbf0e62069bfd/scipy-1.17.0-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:65ec32f3d32dfc48c72df4291345dae4f048749bc8d5203ee0a3f347f96c5ce6", size = 31344101, upload-time = "2026-01-10T21:26:30.25Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/9a/9406aec58268d437636069419e6977af953d1e246df941d42d3720b7277b/scipy-1.17.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:1f9586a58039d7229ce77b52f8472c972448cded5736eaf102d5658bbac4c269", size = 27950385, upload-time = "2026-01-10T21:26:36.801Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/98/e7342709e17afdfd1b26b56ae499ef4939b45a23a00e471dfb5375eea205/scipy-1.17.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:9fad7d3578c877d606b1150135c2639e9de9cecd3705caa37b66862977cc3e72", size = 20122115, upload-time = "2026-01-10T21:26:42.107Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/0e/9eeeb5357a64fd157cbe0302c213517c541cc16b8486d82de251f3c68ede/scipy-1.17.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:423ca1f6584fc03936972b5f7c06961670dbba9f234e71676a7c7ccf938a0d61", size = 22442402, upload-time = "2026-01-10T21:26:48.029Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/10/be13397a0e434f98e0c79552b2b584ae5bb1c8b2be95db421533bbca5369/scipy-1.17.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe508b5690e9eaaa9467fc047f833af58f1152ae51a0d0aed67aa5801f4dd7d6", size = 32696338, upload-time = "2026-01-10T21:26:55.521Z" },
+ { url = "https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752", size = 34977201, upload-time = "2026-01-10T21:27:03.501Z" },
+ { url = "https://files.pythonhosted.org/packages/19/5b/1a63923e23ccd20bd32156d7dd708af5bbde410daa993aa2500c847ab2d2/scipy-1.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec3842ec9ac9de5917899b277428886042a93db0b227ebbe3a333b64ec7643d", size = 34777384, upload-time = "2026-01-10T21:27:11.423Z" },
+ { url = "https://files.pythonhosted.org/packages/39/22/b5da95d74edcf81e540e467202a988c50fef41bd2011f46e05f72ba07df6/scipy-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7425fcafbc09a03731e1bc05581f5fad988e48c6a861f441b7ab729a49a55ea", size = 37379586, upload-time = "2026-01-10T21:27:20.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/b6/8ac583d6da79e7b9e520579f03007cb006f063642afd6b2eeb16b890bf93/scipy-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:87b411e42b425b84777718cc41516b8a7e0795abfa8e8e1d573bf0ef014f0812", size = 36287211, upload-time = "2026-01-10T21:28:43.122Z" },
+ { url = "https://files.pythonhosted.org/packages/55/fb/7db19e0b3e52f882b420417644ec81dd57eeef1bd1705b6f689d8ff93541/scipy-1.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:357ca001c6e37601066092e7c89cca2f1ce74e2a520ca78d063a6d2201101df2", size = 24312646, upload-time = "2026-01-10T21:28:49.893Z" },
+ { url = "https://files.pythonhosted.org/packages/20/b6/7feaa252c21cc7aff335c6c55e1b90ab3e3306da3f048109b8b639b94648/scipy-1.17.0-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:ec0827aa4d36cb79ff1b81de898e948a51ac0b9b1c43e4a372c0508c38c0f9a3", size = 31693194, upload-time = "2026-01-10T21:27:27.454Z" },
+ { url = "https://files.pythonhosted.org/packages/76/bb/bbb392005abce039fb7e672cb78ac7d158700e826b0515cab6b5b60c26fb/scipy-1.17.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:819fc26862b4b3c73a60d486dbb919202f3d6d98c87cf20c223511429f2d1a97", size = 28365415, upload-time = "2026-01-10T21:27:34.26Z" },
+ { url = "https://files.pythonhosted.org/packages/37/da/9d33196ecc99fba16a409c691ed464a3a283ac454a34a13a3a57c0d66f3a/scipy-1.17.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:363ad4ae2853d88ebcde3ae6ec46ccca903ea9835ee8ba543f12f575e7b07e4e", size = 20537232, upload-time = "2026-01-10T21:27:40.306Z" },
+ { url = "https://files.pythonhosted.org/packages/56/9d/f4b184f6ddb28e9a5caea36a6f98e8ecd2a524f9127354087ce780885d83/scipy-1.17.0-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:979c3a0ff8e5ba254d45d59ebd38cde48fce4f10b5125c680c7a4bfe177aab07", size = 22791051, upload-time = "2026-01-10T21:27:46.539Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/9d/025cccdd738a72140efc582b1641d0dd4caf2e86c3fb127568dc80444e6e/scipy-1.17.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:130d12926ae34399d157de777472bf82e9061c60cc081372b3118edacafe1d00", size = 32815098, upload-time = "2026-01-10T21:27:54.389Z" },
+ { url = "https://files.pythonhosted.org/packages/48/5f/09b879619f8bca15ce392bfc1894bd9c54377e01d1b3f2f3b595a1b4d945/scipy-1.17.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e886000eb4919eae3a44f035e63f0fd8b651234117e8f6f29bad1cd26e7bc45", size = 35031342, upload-time = "2026-01-10T21:28:03.012Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/9a/f0f0a9f0aa079d2f106555b984ff0fbb11a837df280f04f71f056ea9c6e4/scipy-1.17.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:13c4096ac6bc31d706018f06a49abe0485f96499deb82066b94d19b02f664209", size = 34893199, upload-time = "2026-01-10T21:28:10.832Z" },
+ { url = "https://files.pythonhosted.org/packages/90/b8/4f0f5cf0c5ea4d7548424e6533e6b17d164f34a6e2fb2e43ffebb6697b06/scipy-1.17.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cacbaddd91fcffde703934897c5cd2c7cb0371fac195d383f4e1f1c5d3f3bd04", size = 37438061, upload-time = "2026-01-10T21:28:19.684Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/cc/2bd59140ed3b2fa2882fb15da0a9cb1b5a6443d67cfd0d98d4cec83a57ec/scipy-1.17.0-cp313-cp313t-win_amd64.whl", hash = "sha256:edce1a1cf66298cccdc48a1bdf8fb10a3bf58e8b58d6c3883dd1530e103f87c0", size = 36328593, upload-time = "2026-01-10T21:28:28.007Z" },
+ { url = "https://files.pythonhosted.org/packages/13/1b/c87cc44a0d2c7aaf0f003aef2904c3d097b422a96c7e7c07f5efd9073c1b/scipy-1.17.0-cp313-cp313t-win_arm64.whl", hash = "sha256:30509da9dbec1c2ed8f168b8d8aa853bc6723fede1dbc23c7d43a56f5ab72a67", size = 24625083, upload-time = "2026-01-10T21:28:35.188Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/2d/51006cd369b8e7879e1c630999a19d1fbf6f8b5ed3e33374f29dc87e53b3/scipy-1.17.0-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:c17514d11b78be8f7e6331b983a65a7f5ca1fd037b95e27b280921fe5606286a", size = 31346803, upload-time = "2026-01-10T21:28:57.24Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/2e/2349458c3ce445f53a6c93d4386b1c4c5c0c540917304c01222ff95ff317/scipy-1.17.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:4e00562e519c09da34c31685f6acc3aa384d4d50604db0f245c14e1b4488bfa2", size = 27967182, upload-time = "2026-01-10T21:29:04.107Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/7c/df525fbfa77b878d1cfe625249529514dc02f4fd5f45f0f6295676a76528/scipy-1.17.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f7df7941d71314e60a481e02d5ebcb3f0185b8d799c70d03d8258f6c80f3d467", size = 20139125, upload-time = "2026-01-10T21:29:10.179Z" },
+ { url = "https://files.pythonhosted.org/packages/33/11/fcf9d43a7ed1234d31765ec643b0515a85a30b58eddccc5d5a4d12b5f194/scipy-1.17.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:aabf057c632798832f071a8dde013c2e26284043934f53b00489f1773b33527e", size = 22443554, upload-time = "2026-01-10T21:29:15.888Z" },
+ { url = "https://files.pythonhosted.org/packages/80/5c/ea5d239cda2dd3d31399424967a24d556cf409fbea7b5b21412b0fd0a44f/scipy-1.17.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a38c3337e00be6fd8a95b4ed66b5d988bac4ec888fd922c2ea9fe5fb1603dd67", size = 32757834, upload-time = "2026-01-10T21:29:23.406Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/7e/8c917cc573310e5dc91cbeead76f1b600d3fb17cf0969db02c9cf92e3cfa/scipy-1.17.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00fb5f8ec8398ad90215008d8b6009c9db9fa924fd4c7d6be307c6f945f9cd73", size = 34995775, upload-time = "2026-01-10T21:29:31.915Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/43/176c0c3c07b3f7df324e7cdd933d3e2c4898ca202b090bd5ba122f9fe270/scipy-1.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f2a4942b0f5f7c23c7cd641a0ca1955e2ae83dedcff537e3a0259096635e186b", size = 34841240, upload-time = "2026-01-10T21:29:39.995Z" },
+ { url = "https://files.pythonhosted.org/packages/44/8c/d1f5f4b491160592e7f084d997de53a8e896a3ac01cd07e59f43ca222744/scipy-1.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf133ced83889583156566d2bdf7a07ff89228fe0c0cb727f777de92092ec6b", size = 37394463, upload-time = "2026-01-10T21:29:48.723Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/ec/42a6657f8d2d087e750e9a5dde0b481fd135657f09eaf1cf5688bb23c338/scipy-1.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:3625c631a7acd7cfd929e4e31d2582cf00f42fcf06011f59281271746d77e061", size = 37053015, upload-time = "2026-01-10T21:30:51.418Z" },
+ { url = "https://files.pythonhosted.org/packages/27/58/6b89a6afd132787d89a362d443a7bddd511b8f41336a1ae47f9e4f000dc4/scipy-1.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:9244608d27eafe02b20558523ba57f15c689357c85bdcfe920b1828750aa26eb", size = 24951312, upload-time = "2026-01-10T21:30:56.771Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/01/f58916b9d9ae0112b86d7c3b10b9e685625ce6e8248df139d0fcb17f7397/scipy-1.17.0-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:2b531f57e09c946f56ad0b4a3b2abee778789097871fc541e267d2eca081cff1", size = 31706502, upload-time = "2026-01-10T21:29:56.326Z" },
+ { url = "https://files.pythonhosted.org/packages/59/8e/2912a87f94a7d1f8b38aabc0faf74b82d3b6c9e22be991c49979f0eceed8/scipy-1.17.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:13e861634a2c480bd237deb69333ac79ea1941b94568d4b0efa5db5e263d4fd1", size = 28380854, upload-time = "2026-01-10T21:30:01.554Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/1c/874137a52dddab7d5d595c1887089a2125d27d0601fce8c0026a24a92a0b/scipy-1.17.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:eb2651271135154aa24f6481cbae5cc8af1f0dd46e6533fb7b56aa9727b6a232", size = 20552752, upload-time = "2026-01-10T21:30:05.93Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/f0/7518d171cb735f6400f4576cf70f756d5b419a07fe1867da34e2c2c9c11b/scipy-1.17.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:c5e8647f60679790c2f5c76be17e2e9247dc6b98ad0d3b065861e082c56e078d", size = 22803972, upload-time = "2026-01-10T21:30:10.651Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/74/3498563a2c619e8a3ebb4d75457486c249b19b5b04a30600dfd9af06bea5/scipy-1.17.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fb10d17e649e1446410895639f3385fd2bf4c3c7dfc9bea937bddcbc3d7b9ba", size = 32829770, upload-time = "2026-01-10T21:30:16.359Z" },
+ { url = "https://files.pythonhosted.org/packages/48/d1/7b50cedd8c6c9d6f706b4b36fa8544d829c712a75e370f763b318e9638c1/scipy-1.17.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8547e7c57f932e7354a2319fab613981cde910631979f74c9b542bb167a8b9db", size = 35051093, upload-time = "2026-01-10T21:30:22.987Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/82/a2d684dfddb87ba1b3ea325df7c3293496ee9accb3a19abe9429bce94755/scipy-1.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33af70d040e8af9d5e7a38b5ed3b772adddd281e3062ff23fec49e49681c38cf", size = 34909905, upload-time = "2026-01-10T21:30:28.704Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/5e/e565bd73991d42023eb82bb99e51c5b3d9e2c588ca9d4b3e2cc1d3ca62a6/scipy-1.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9eb55bb97d00f8b7ab95cb64f873eb0bf54d9446264d9f3609130381233483f", size = 37457743, upload-time = "2026-01-10T21:30:34.819Z" },
+ { url = "https://files.pythonhosted.org/packages/58/a8/a66a75c3d8f1fb2b83f66007d6455a06a6f6cf5618c3dc35bc9b69dd096e/scipy-1.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1ff269abf702f6c7e67a4b7aad981d42871a11b9dd83c58d2d2ea624efbd1088", size = 37098574, upload-time = "2026-01-10T21:30:40.782Z" },
+ { url = "https://files.pythonhosted.org/packages/56/a5/df8f46ef7da168f1bc52cd86e09a9de5c6f19cc1da04454d51b7d4f43408/scipy-1.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:031121914e295d9791319a1875444d55079885bbae5bdc9c5e0f2ee5f09d34ff", size = 25246266, upload-time = "2026-01-10T21:30:45.923Z" },
+]
+
+[[package]]
+name = "seaborn"
+version = "0.13.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "matplotlib" },
+ { name = "numpy" },
+ { name = "pandas" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" },
+]
+
+[[package]]
+name = "semantic-version"
+version = "2.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" },
+]
+
+[[package]]
+name = "sentence-transformers"
+version = "5.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "scikit-learn" },
+ { name = "scipy" },
+ { name = "torch" },
+ { name = "tqdm" },
+ { name = "transformers" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/4d/64abf2856ffb704bcaea89a781a081187cc602c1fc7cd9ef1fc8ebf567be/sentence_transformers-5.2.1.tar.gz", hash = "sha256:9c676411becbf2a0e7515501788a7f84b99ca2dcc124ce392639bcdda4a256ff", size = 381393, upload-time = "2026-01-26T14:48:05.265Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bf/2c/a75cc1e2b48914fe709a6aec3799ceeb0658ec87058d2e3f950d54cc1d06/sentence_transformers-5.2.1-py3-none-any.whl", hash = "sha256:388483e174223958ae71b6888c49a6cc1480306a12655a690b605c7a176162d0", size = 493822, upload-time = "2026-01-26T14:48:03.881Z" },
+]
+
+[[package]]
+name = "sentry-sdk"
+version = "2.50.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "certifi" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/15/8a/3c4f53d32c21012e9870913544e56bfa9e931aede080779a0f177513f534/sentry_sdk-2.50.0.tar.gz", hash = "sha256:873437a989ee1b8b25579847bae8384515bf18cfed231b06c591b735c1781fe3", size = 401233, upload-time = "2026-01-20T12:53:16.244Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/5b/cbc2bb9569f03c8e15d928357e7e6179e5cfab45544a3bbac8aec4caf9be/sentry_sdk-2.50.0-py2.py3-none-any.whl", hash = "sha256:0ef0ed7168657ceb5a0be081f4102d92042a125462d1d1a29277992e344e749e", size = 424961, upload-time = "2026-01-20T12:53:14.826Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "80.10.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" },
+]
+
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "smart-open"
+version = "7.5.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "wrapt" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/9a/0a7acb748b86e2922982366d780ca4b16c33f7246fa5860d26005c97e4f3/smart_open-7.5.0.tar.gz", hash = "sha256:f394b143851d8091011832ac8113ea4aba6b92e6c35f6e677ddaaccb169d7cb9", size = 53920, upload-time = "2025-11-08T21:38:40.698Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/95/bc978be7ea0babf2fb48a414b6afaad414c6a9e8b1eafc5b8a53c030381a/smart_open-7.5.0-py3-none-any.whl", hash = "sha256:87e695c5148bbb988f15cec00971602765874163be85acb1c9fb8abc012e6599", size = 63940, upload-time = "2025-11-08T21:38:39.024Z" },
+]
+
+[[package]]
+name = "smmap"
+version = "5.0.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" },
+]
+
+[[package]]
+name = "sniffio"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
+]
+
+[[package]]
+name = "soupsieve"
+version = "2.8.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
+]
+
+[[package]]
+name = "spacy"
+version = "3.8.11"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "catalogue" },
+ { name = "cymem" },
+ { name = "jinja2" },
+ { name = "murmurhash" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "preshed" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "setuptools" },
+ { name = "spacy-legacy" },
+ { name = "spacy-loggers" },
+ { name = "srsly" },
+ { name = "thinc" },
+ { name = "tqdm" },
+ { name = "typer-slim" },
+ { name = "wasabi" },
+ { name = "weasel" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/59/9f/424244b0e2656afc9ff82fb7a96931a47397bfce5ba382213827b198312a/spacy-3.8.11.tar.gz", hash = "sha256:54e1e87b74a2f9ea807ffd606166bf29ac45e2bd81ff7f608eadc7b05787d90d", size = 1326804, upload-time = "2025-11-17T20:40:03.079Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/fb/01eadf4ba70606b3054702dc41fc2ccf7d70fb14514b3cd57f0ff78ebea8/spacy-3.8.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aa1ee8362074c30098feaaf2dd888c829a1a79c4311eec1b117a0a61f16fa6dd", size = 6073726, upload-time = "2025-11-17T20:39:01.679Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/f8/07b03a2997fc2621aaeafae00af50f55522304a7da6926b07027bb6d0709/spacy-3.8.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75a036d04c2cf11d6cb566c0a689860cc5a7a75b439e8fea1b3a6b673dabf25d", size = 5724702, upload-time = "2025-11-17T20:39:03.486Z" },
+ { url = "https://files.pythonhosted.org/packages/13/0c/c4fa0f379dbe3258c305d2e2df3760604a9fcd71b34f8f65c23e43f4cf55/spacy-3.8.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cb599d2747d4a59a5f90e8a453c149b13db382a8297925cf126333141dbc4f7", size = 32727774, upload-time = "2025-11-17T20:39:05.894Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/8e/6a4ba82bed480211ebdf5341b0f89e7271b454307525ac91b5e447825914/spacy-3.8.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:94632e302ad2fb79dc285bf1e9e4d4a178904d5c67049e0e02b7fb4a77af85c4", size = 33215053, upload-time = "2025-11-17T20:39:08.588Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/bc/44d863d248e9d7358c76a0aa8b3f196b8698df520650ed8de162e18fbffb/spacy-3.8.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aeca6cf34009d48cda9fb1bbfb532469e3d643817241a73e367b34ab99a5806f", size = 32074195, upload-time = "2025-11-17T20:39:11.601Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/7d/0b115f3f16e1dd2d3f99b0f89497867fc11c41aed94f4b7a4367b4b54136/spacy-3.8.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:368a79b8df925b15d89dccb5e502039446fb2ce93cf3020e092d5b962c3349b9", size = 32996143, upload-time = "2025-11-17T20:39:14.705Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/48/7e9581b476df76aaf9ee182888d15322e77c38b0bbbd5e80160ba0bddd4c/spacy-3.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:88d65941a87f58d75afca1785bd64d01183a92f7269dcbcf28bd9d6f6a77d1a7", size = 14217511, upload-time = "2025-11-17T20:39:17.316Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/1f/307a16f32f90aa5ee7ad8d29ff8620a57132b80a4c8c536963d46d192e1a/spacy-3.8.11-cp312-cp312-win_arm64.whl", hash = "sha256:97b865d6d3658e2ab103a67d6c8a2d678e193e84a07f40d9938565b669ceee39", size = 13614446, upload-time = "2025-11-17T20:39:19.748Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/5c/3f07cff8bc478fcf48a915ca9fe8637486a1ec676587ed3e6fd775423301/spacy-3.8.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea4adeb399636059925be085c5bb852c1f3a2ebe1c2060332cbad6257d223bbc", size = 6051355, upload-time = "2025-11-17T20:39:22.243Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/44/4671e8098b62befec69c7848538a0824086559f74065284bbd57a5747781/spacy-3.8.11-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dd785e6bd85a58fa037da0c18fcd7250e2daecdfc30464d3882912529d1ad588", size = 5700468, upload-time = "2025-11-17T20:39:23.87Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/98/5708bdfb39f94af0655568e14d953886117e18bd04c3aa3ab5ff1a60ea89/spacy-3.8.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:598c177054eb6196deed03cac6fb7a3229f4789719ad0c9f7483f9491e375749", size = 32521877, upload-time = "2025-11-17T20:39:26.291Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/1f/731beb48f2c7415a71e2f655876fea8a0b3a6798be3d4d51b794f939623d/spacy-3.8.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a5a449ed3f2d03399481870b776f3ec61f2b831812d63dc1acedf6da70e5ab03", size = 32848355, upload-time = "2025-11-17T20:39:28.971Z" },
+ { url = "https://files.pythonhosted.org/packages/47/6b/f3d131d3f9bb1c7de4f355a12adcd0a5fa77f9f624711ddd0f19c517e88b/spacy-3.8.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a6c35c2cb93bade9b7360d1f9db608a066246a41301bb579309efb50764ba55b", size = 31764944, upload-time = "2025-11-17T20:39:31.788Z" },
+ { url = "https://files.pythonhosted.org/packages/72/bf/37ea8134667a4f2787b5f0e0146f2e8df1fb36ab67d598ad06eb5ed2e7db/spacy-3.8.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0156ae575b20290021573faa1fed8a82b11314e9a1c28f034713359a5240a325", size = 32718517, upload-time = "2025-11-17T20:39:35.286Z" },
+ { url = "https://files.pythonhosted.org/packages/79/fe/436435dfa93cc355ed511f21cf3cda5302b7aa29716457317eb07f1cf2da/spacy-3.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:6f39cf36f86bd6a8882076f86ca80f246c73aa41d7ebc8679fbbe41b6f8ec045", size = 14211913, upload-time = "2025-11-17T20:39:37.906Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/23/f89cfa51f54aa5e9c6c7a37f8bf4952d678f0902a5e1d81dfda33a94bfb2/spacy-3.8.11-cp313-cp313-win_arm64.whl", hash = "sha256:9a7151eee0814a5ced36642b42b1ecc8f98ac7225f3e378fb9f862ffbe84b8bf", size = 13605169, upload-time = "2025-11-17T20:39:40.455Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/78/ddeb09116b593f3cccc7eb489a713433076b11cf8cdfb98aec641b73a2c2/spacy-3.8.11-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:43c24d19a3f85bde0872935294a31fd9b3a6db3f92bb2b75074177cd3acec03f", size = 6067734, upload-time = "2025-11-17T20:39:42.629Z" },
+ { url = "https://files.pythonhosted.org/packages/65/bb/1bb630250dc70e00fa3821879c6e2cb65c19425aba38840d3484061285c1/spacy-3.8.11-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b6158c21da57b8373d2d1afb2b73977c4bc4235d2563e7788d44367fc384939a", size = 5732963, upload-time = "2025-11-17T20:39:44.872Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/56/c58071b3db23932ab2b934af3462a958e7edf472da9668e4869fe2a2199e/spacy-3.8.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1c0bd1bde1d91f1d7a44774ca4ca3fcf064946b72599a8eb34c25e014362ace1", size = 32447290, upload-time = "2025-11-17T20:39:47.392Z" },
+ { url = "https://files.pythonhosted.org/packages/34/eb/d3947efa2b46848372e89ced8371671d77219612a3eebef15db5690aa4d2/spacy-3.8.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:99b767c41a772e544cf2d48e0808764f42f17eb2fd6188db4a729922ff7f0c1e", size = 32488011, upload-time = "2025-11-17T20:39:50.408Z" },
+ { url = "https://files.pythonhosted.org/packages/04/9e/8c6c01558b62388557247e553e48874f52637a5648b957ed01fbd628391d/spacy-3.8.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3c500f04c164e4366a1163a61bf39fd50f0c63abdb1fc17991281ec52a54ab4", size = 31731340, upload-time = "2025-11-17T20:39:53.221Z" },
+ { url = "https://files.pythonhosted.org/packages/23/1f/21812ec34b187ef6ba223389760dfea09bbe27d2b84b553c5205576b4ac2/spacy-3.8.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a2bfe45c0c1530eaabc68f5434c52b1be8df10d5c195c54d4dc2e70cea97dc65", size = 32478557, upload-time = "2025-11-17T20:39:55.826Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/16/a0c9174a232dfe7b48281c05364957e2c6d0f80ef26b67ce8d28a49c2d91/spacy-3.8.11-cp314-cp314-win_amd64.whl", hash = "sha256:45d0bbc8442d18dcea9257be0d1ab26e884067e038b1fa133405bf2f20c74edf", size = 14396041, upload-time = "2025-11-17T20:39:58.557Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/d0/a6aad5b73d523e4686474b0cfcf46f37f3d7a18765be5c1f56c1dcee4c18/spacy-3.8.11-cp314-cp314-win_arm64.whl", hash = "sha256:90a12961ecc44e0195fd42db9f0ce4aade17e6fe03f8ab98d4549911d9e6f992", size = 13823760, upload-time = "2025-11-17T20:40:00.831Z" },
+]
+
+[[package]]
+name = "spacy-legacy"
+version = "3.0.12"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d9/79/91f9d7cc8db5642acad830dcc4b49ba65a7790152832c4eceb305e46d681/spacy-legacy-3.0.12.tar.gz", hash = "sha256:b37d6e0c9b6e1d7ca1cf5bc7152ab64a4c4671f59c85adaf7a3fcb870357a774", size = 23806, upload-time = "2023-01-23T09:04:15.104Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c3/55/12e842c70ff8828e34e543a2c7176dac4da006ca6901c9e8b43efab8bc6b/spacy_legacy-3.0.12-py2.py3-none-any.whl", hash = "sha256:476e3bd0d05f8c339ed60f40986c07387c0a71479245d6d0f4298dbd52cda55f", size = 29971, upload-time = "2023-01-23T09:04:13.45Z" },
+]
+
+[[package]]
+name = "spacy-loggers"
+version = "1.0.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/67/3d/926db774c9c98acf66cb4ed7faf6c377746f3e00b84b700d0868b95d0712/spacy-loggers-1.0.5.tar.gz", hash = "sha256:d60b0bdbf915a60e516cc2e653baeff946f0cfc461b452d11a4d5458c6fe5f24", size = 20811, upload-time = "2023-09-11T12:26:52.323Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645", size = 22343, upload-time = "2023-09-11T12:26:50.586Z" },
+]
+
+[[package]]
+name = "srsly"
+version = "2.5.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "catalogue" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cf/77/5633c4ba65e3421b72b5b4bd93aa328360b351b3a1e5bf3c90eb224668e5/srsly-2.5.2.tar.gz", hash = "sha256:4092bc843c71b7595c6c90a0302a197858c5b9fe43067f62ae6a45bc3baa1c19", size = 492055, upload-time = "2025-11-17T14:11:02.543Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8f/1c/21f658d98d602a559491b7886c7ca30245c2cd8987ff1b7709437c0f74b1/srsly-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f92b4f883e6be4ca77f15980b45d394d310f24903e25e1b2c46df783c7edcce", size = 656161, upload-time = "2025-11-17T14:10:03.181Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/a2/bc6fd484ed703857043ae9abd6c9aea9152f9480a6961186ee6c1e0c49e8/srsly-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac4790a54b00203f1af5495b6b8ac214131139427f30fcf05cf971dde81930eb", size = 653237, upload-time = "2025-11-17T14:10:04.636Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ea/e3895da29a15c8d325e050ad68a0d1238eece1d2648305796adf98dcba66/srsly-2.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce5c6b016050857a7dd365c9dcdd00d96e7ac26317cfcb175db387e403de05bf", size = 1174418, upload-time = "2025-11-17T14:10:05.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/a5/21996231f53ee97191d0746c3a672ba33a4d86a19ffad85a1c0096c91c5f/srsly-2.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:539c6d0016e91277b5e9be31ebed03f03c32580d49c960e4a92c9003baecf69e", size = 1183089, upload-time = "2025-11-17T14:10:07.335Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/df/eb17aa8e4a828e8df7aa7dc471295529d9126e6b710f1833ebe0d8568a8e/srsly-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f24b2c4f4c29da04083f09158543eb3f8893ba0ac39818693b3b259ee8044f0", size = 1122594, upload-time = "2025-11-17T14:10:08.899Z" },
+ { url = "https://files.pythonhosted.org/packages/80/74/1654a80e6c8ec3ee32370ea08a78d3651e0ba1c4d6e6be31c9efdb9a2d10/srsly-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d34675047460a3f6999e43478f40d9b43917ea1e93a75c41d05bf7648f3e872d", size = 1139594, upload-time = "2025-11-17T14:10:10.286Z" },
+ { url = "https://files.pythonhosted.org/packages/73/aa/8393344ca7f0e81965febba07afc5cad68335ed0426408d480b861ab915b/srsly-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:81fd133ba3c66c07f0e3a889d2b4c852984d71ea833a665238a9d47d8e051ba5", size = 654750, upload-time = "2025-11-17T14:10:11.637Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c5/dc29e65419692444253ea549106be156c5911041f16791f3b62fb90c14f2/srsly-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d976d6ae8e66006797b919e3d58533dce64cd48a5447a8ff7277f9b0505b0185", size = 654723, upload-time = "2025-11-17T14:10:13.305Z" },
+ { url = "https://files.pythonhosted.org/packages/80/8c/8111e7e8c766b47b5a5f9864f27f532cf6bb92837a3e277eb297170bd6af/srsly-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:24f52ecd27409ea24ba116ee9f07a2bb1c4b9ba11284b32a0bf2ca364499d1c1", size = 651651, upload-time = "2025-11-17T14:10:14.907Z" },
+ { url = "https://files.pythonhosted.org/packages/45/de/3f99d4e44af427ee09004df6586d0746640536b382c948f456be027c599b/srsly-2.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b0667ce1effb32a57522db10705db7c78d144547fcacc8a06df62c4bb7f96e", size = 1158012, upload-time = "2025-11-17T14:10:16.176Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/2f/66044ef5a10a487652913c1a7f32396cb0e9e32ecfc3fdc0a0bc0382e703/srsly-2.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60782f6f79c340cdaf1ba7cbaa1d354a0f7c8f86b285f1e14e75edb51452895a", size = 1163258, upload-time = "2025-11-17T14:10:17.471Z" },
+ { url = "https://files.pythonhosted.org/packages/74/6b/698834048672b52937e8cf09b554adb81b106c0492f9bc62e41e3b46a69b/srsly-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:eec51abb1b58e1e6c689714104aeeba6290c40c0bfad0243b9b594df89f05881", size = 1112214, upload-time = "2025-11-17T14:10:18.679Z" },
+ { url = "https://files.pythonhosted.org/packages/85/17/1efc70426be93d32a3c6c5c12d795eb266a9255d8b537fcb924a3de57fcb/srsly-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:76464e45f73afd20c2c34d2ef145bf788afc32e7d45f36f6393ed92a85189ed3", size = 1130687, upload-time = "2025-11-17T14:10:20.346Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/25/07f8c8a778bc0447ee15e37089b08af81b24fcc1d4a2c09eff4c3a79b241/srsly-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:009424a96d763951e4872b36ba38823f973bef094a1adbc11102e23e8d1ef429", size = 653128, upload-time = "2025-11-17T14:10:21.552Z" },
+ { url = "https://files.pythonhosted.org/packages/39/03/3d248f538abc141d9c7ed1aa10e61506c0f95515a61066ee90e888f0cd8f/srsly-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a0911dcf1026f982bd8c5f73e1c43f1bc868416408fcbc1f3d99eb59475420c5", size = 659866, upload-time = "2025-11-17T14:10:22.811Z" },
+ { url = "https://files.pythonhosted.org/packages/43/22/0fcff4c977ddfb32a6b10f33d904868b16ce655323756281f973c5a3449e/srsly-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f0ff3ac2942aee44235ca3c7712fcbd6e0d1a092e10ee16e07cef459ed6d7f65", size = 655868, upload-time = "2025-11-17T14:10:24.036Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/c1/e158f26a5597ac31b0f306d2584411ec1f984058e8171d76c678bf439e96/srsly-2.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:78385fb75e1bf7b81ffde97555aee094d270a5e0ea66f8280f6e95f5bb508b3e", size = 1156753, upload-time = "2025-11-17T14:10:25.366Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/bc/2001cd27fd6ecdae79050cf6b655ca646dedc0b69a756e6a87993cc47314/srsly-2.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2e9943b70bd7655b9eefca77aab838c3b7acea00c9dd244fd218a43dc61c518b", size = 1157916, upload-time = "2025-11-17T14:10:26.705Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/dd/56f563c2d0cd76c8fd22fb9f1589f18af50b54d31dd3323ceb05fe7999b8/srsly-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7d235a2bb08f5240e47c6aba4d9688b228d830fbf4c858388d9c151a10039e6d", size = 1114582, upload-time = "2025-11-17T14:10:27.997Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e6/e155facc965a119e6f5d32b7e95082cadfb62cc5d97087d53db93f3a5a98/srsly-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ad94ee18b3042a6cdfdc022556e2ed9a7b52b876de86fe334c4d8ec58d59ecbc", size = 1129875, upload-time = "2025-11-17T14:10:29.295Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/3a/c12a4d556349c9f491b0a9d27968483f22934d2a02dfb14fb1d3a7d9b837/srsly-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:6658467165d8fa4aec0f5f6e2da8fe977e087eaff13322b0ff20450f0d762cee", size = 658858, upload-time = "2025-11-17T14:10:30.612Z" },
+ { url = "https://files.pythonhosted.org/packages/70/db/52510cbf478ab3ae8cb6c95aff3a499f2ded69df6d84df8a293630e9f10a/srsly-2.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:517e907792acf574979752ce33e7b15985c95d4ed7d8e38ee47f36063dc985ac", size = 666843, upload-time = "2025-11-17T14:10:32.082Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/da/4257b1d4c3eb005ecd135414398c033c13c4d3dffb715f63c3acd63d8d1a/srsly-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5602797e6f87bf030b11ad356828142367c5c81e923303b5ff2a88dfb12d1e4", size = 663981, upload-time = "2025-11-17T14:10:33.542Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/f8/1ec5edd7299d8599def20fc3440372964f7c750022db8063e321747d1cf8/srsly-2.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3452306118f8604daaaac6d770ee8f910fca449e8f066dcc96a869b43ece5340", size = 1267808, upload-time = "2025-11-17T14:10:35.285Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/5c/4ef9782c9a3f331ef80e1ea8fc6fab50fc3d32ae61a494625d2c5f30cc4c/srsly-2.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e2d59f1ce00d73397a7f5b9fc33e76d17816ce051abe4eb920cec879d2a9d4f4", size = 1252838, upload-time = "2025-11-17T14:10:37.024Z" },
+ { url = "https://files.pythonhosted.org/packages/39/da/d13cfc662d71eec3ccd4072433bf435bd2e11e1c5340150b4cc43fad46f4/srsly-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ebda3736651d33d92b17e26c525ba8d0b94d0ee379c9f92e8d937ba89dca8978", size = 1244558, upload-time = "2025-11-17T14:10:38.73Z" },
+ { url = "https://files.pythonhosted.org/packages/26/50/92bf62dfb19532b823ef52251bb7003149e1d4a89f50a63332c8ff5f894b/srsly-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:74a9338fcc044f4bdc7113b2d9db2db8e0a263c69f1cba965acf12c845d8b365", size = 1244935, upload-time = "2025-11-17T14:10:42.324Z" },
+ { url = "https://files.pythonhosted.org/packages/95/81/6ea10ef6228ce4438a240c803639f7ccf5eae3469fbc015f33bd84aa8df1/srsly-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:8e2b9058623c44b07441eb0d711dfdf6302f917f0634d0a294cae37578dcf899", size = 676105, upload-time = "2025-11-17T14:10:43.633Z" },
+]
+
+[[package]]
+name = "sse-starlette"
+version = "3.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "starlette" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8b/8d/00d280c03ffd39aaee0e86ec81e2d3b9253036a0f93f51d10503adef0e65/sse_starlette-3.2.0.tar.gz", hash = "sha256:8127594edfb51abe44eac9c49e59b0b01f1039d0c7461c6fd91d4e03b70da422", size = 27253, upload-time = "2026-01-17T13:11:05.62Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/96/7f/832f015020844a8b8f7a9cbc103dd76ba8e3875004c41e08440ea3a2b41a/sse_starlette-3.2.0-py3-none-any.whl", hash = "sha256:5876954bd51920fc2cd51baee47a080eb88a37b5b784e615abb0b283f801cdbf", size = 12763, upload-time = "2026-01-17T13:11:03.775Z" },
+]
+
+[[package]]
+name = "starlette"
+version = "0.50.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
+[[package]]
+name = "tavily-python"
+version = "0.7.19"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "httpx" },
+ { name = "requests" },
+ { name = "tiktoken" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bd/e1/21639fc5904d52d66f217e3031cd28d1e2227fd3f3f0b7fb51582d2f6d26/tavily_python-0.7.19.tar.gz", hash = "sha256:b5c195f58f8df0886814e50e641ba10ef8e5ec72be1e884b8f4b3b99c49934cf", size = 21458, upload-time = "2026-01-15T15:11:33.386Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/73/51/d6056841a4b440ba0a1703a9aff3a87b8be57c6bb5c42c6615696f01a890/tavily_python-0.7.19-py3-none-any.whl", hash = "sha256:b3d95041f22c5dff37255f65308379e4f8ccef8fe0d92b860815812b570f5ef2", size = 18340, upload-time = "2026-01-15T15:11:29.976Z" },
+]
+
+[[package]]
+name = "tensorboard"
+version = "2.20.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "absl-py" },
+ { name = "grpcio" },
+ { name = "markdown" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "protobuf" },
+ { name = "setuptools" },
+ { name = "tensorboard-data-server" },
+ { name = "werkzeug" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" },
+]
+
+[[package]]
+name = "tensorboard-data-server"
+version = "0.7.2"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" },
+ { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" },
+]
+
+[[package]]
+name = "textual"
+version = "7.4.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown-it-py", extra = ["linkify"] },
+ { name = "mdit-py-plugins" },
+ { name = "platformdirs" },
+ { name = "pygments" },
+ { name = "rich" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/64/8d/2fbd6b8652f4cabf9cb0852d7af1aa45b6cad32d0f50735856e8f9e41719/textual-7.4.0.tar.gz", hash = "sha256:1a9598e485492f9a8f033c7ec5e59528df3ab0742fda925681acf78b0fb210de", size = 1592252, upload-time = "2026-01-25T19:57:04.624Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/9c/4169ccffed6d53f78e3175eae0cd649990071c6e24b6ad8830812ebab726/textual-7.4.0-py3-none-any.whl", hash = "sha256:41a066cae649654d4ecfe53b8316f5737c0042d1693ce50690b769a7840780ac", size = 717985, upload-time = "2026-01-25T19:57:02.966Z" },
+]
+
+[[package]]
+name = "thinc"
+version = "8.3.10"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "blis" },
+ { name = "catalogue" },
+ { name = "confection" },
+ { name = "cymem" },
+ { name = "murmurhash" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "preshed" },
+ { name = "pydantic" },
+ { name = "setuptools" },
+ { name = "srsly" },
+ { name = "wasabi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2f/3a/2d0f0be132b9faaa6d56f04565ae122684273e4bf4eab8dee5f48dc00f68/thinc-8.3.10.tar.gz", hash = "sha256:5a75109f4ee1c968fc055ce651a17cb44b23b000d9e95f04a4d047ab3cb3e34e", size = 194196, upload-time = "2025-11-17T17:21:46.435Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d3/34/ba3b386d92edf50784b60ee34318d47c7f49c198268746ef7851c5bbe8cf/thinc-8.3.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51bc6ef735bdbcab75ab2916731b8f61f94c66add6f9db213d900d3c6a244f95", size = 794509, upload-time = "2025-11-17T17:21:03.21Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f3/9f52d18115cd9d8d7b2590d226cb2752d2a5ffec61576b19462b48410184/thinc-8.3.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4f48b4d346915f98e9722c0c50ef911cc16c6790a2b7afebc6e1a2c96a6ce6c6", size = 741084, upload-time = "2025-11-17T17:21:04.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/9c/129c2b740c4e3d3624b6fb3dec1577ef27cb804bc1647f9bc3e1801ea20c/thinc-8.3.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5003f4db2db22cc8d686db8db83509acc3c50f4c55ebdcb2bbfcc1095096f7d2", size = 3846337, upload-time = "2025-11-17T17:21:06.079Z" },
+ { url = "https://files.pythonhosted.org/packages/22/d2/738cf188dea8240c2be081c83ea47270fea585eba446171757d2cdb9b675/thinc-8.3.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b12484c3ed0632331fada2c334680dd6bc35972d0717343432dfc701f04a9b4c", size = 3901216, upload-time = "2025-11-17T17:21:07.842Z" },
+ { url = "https://files.pythonhosted.org/packages/22/92/32f66eb9b1a29b797bf378a0874615d810d79eefca1d6c736c5ca3f8b918/thinc-8.3.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8677c446d3f9b97a465472c58683b785b25dfcf26c683e3f4e8f8c7c188e4362", size = 4827286, upload-time = "2025-11-17T17:21:09.62Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5f/7ceae1e1f2029efd67ed88e23cd6dc13a5ee647cdc2b35113101b2a62c10/thinc-8.3.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:759c385ac08dcf950238b60b96a28f9c04618861141766928dff4a51b1679b25", size = 5024421, upload-time = "2025-11-17T17:21:11.199Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/66/30f9d8d41049b78bc614213d492792fbcfeb1b28642adf661c42110a7ebd/thinc-8.3.10-cp312-cp312-win_amd64.whl", hash = "sha256:bf3f188c3fa1fdcefd547d1f90a1245c29025d6d0e3f71d7fdf21dad210b990c", size = 1718631, upload-time = "2025-11-17T17:21:12.965Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/44/32e2a5018a1165a304d25eb9b1c74e5310da19a533a35331e8d824dc6a88/thinc-8.3.10-cp312-cp312-win_arm64.whl", hash = "sha256:234b7e57a6ef4e0260d99f4e8fdc328ed12d0ba9bbd98fdaa567294a17700d1c", size = 1642224, upload-time = "2025-11-17T17:21:14.371Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fc/17a2818d1f460b8c4f33b8bd3f21b19d263a647bfd23b572768d175e6b64/thinc-8.3.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c7c3a50ddd423d1c49419899acef4ac80d800af3b423593acb9e40578384b543", size = 789771, upload-time = "2025-11-17T17:21:15.784Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/24/649f54774b1fbe791a1c2efd7d7f0a95cfd9244902553ca7dcf19daab1dd/thinc-8.3.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a1cb110398f51fc2b9a07a2a4daec6f91e166533a9c9f1c565225330f46569a", size = 737051, upload-time = "2025-11-17T17:21:17.933Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/8c/5840c6c504c1fa9718e1c74d6e04d77a474f594888867dbba53f9317285f/thinc-8.3.10-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42318746a67403d04be57d862fe0c0015b58b6fb9bbbf7b6db01f3f103b73a99", size = 3839221, upload-time = "2025-11-17T17:21:20.003Z" },
+ { url = "https://files.pythonhosted.org/packages/45/ef/e7fca88074cb0aa1c1a23195470b4549492c2797fe7dc9ff79a85500153a/thinc-8.3.10-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6b0e41e79973f8828adead770f885db8d0f199bfbaa9591d1d896c385842e993", size = 3885024, upload-time = "2025-11-17T17:21:21.735Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/eb/805e277aa019896009028d727460f071c6cf83843d70f6a69e58994d2203/thinc-8.3.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9ed982daa1eddbad813bfd079546483b849a68b98c01ad4a7e4efd125ddc5d7b", size = 4815939, upload-time = "2025-11-17T17:21:23.942Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/f5/6425f12a60e3782091c9ec16394b9239f0c18c52c70218f3c8c047ff985c/thinc-8.3.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d22bd381410749dec5f629b3162b7d1f1e2d9b7364fd49a7ea555b61c93772b9", size = 5020260, upload-time = "2025-11-17T17:21:25.507Z" },
+ { url = "https://files.pythonhosted.org/packages/85/a2/ae98feffe0b161400e87b7bfc8859e6fa1e6023fa7bcfa0a8cacd83b39a1/thinc-8.3.10-cp313-cp313-win_amd64.whl", hash = "sha256:9c32830446a57da13b6856cacb0225bc2f2104f279d9928d40500081c13aa9ec", size = 1717562, upload-time = "2025-11-17T17:21:27.468Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/e0/faa1d04a6890ea33b9541727d2a3ca88bad794a89f73b9111af6f9aefe10/thinc-8.3.10-cp313-cp313-win_arm64.whl", hash = "sha256:aa43f9af76781d32f5f9fe29299204c8841d71e64cbb56e0e4f3d1e0387c2783", size = 1641536, upload-time = "2025-11-17T17:21:30.129Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/32/7a96e1f2cac159d778c6b0ab4ddd8a139bb57c602cef793b7606cd32428d/thinc-8.3.10-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:44d7038a5d28572105332b44ec9c4c3b6f7953b41d224588ad0473c9b79ccf9e", size = 793037, upload-time = "2025-11-17T17:21:32.538Z" },
+ { url = "https://files.pythonhosted.org/packages/12/d8/81e8495e8ef412767c09d1f9d0d86dc60cd22e6ed75e61b49fbf1dcfcd65/thinc-8.3.10-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:639f20952af722cb0ab4c3d8a00e661686b60c04f82ef48d12064ceda3b8cd0c", size = 740768, upload-time = "2025-11-17T17:21:34.852Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/6d/716488a301d65c5463e92cb0eddae3672ca84f1d70937808cea9760f759c/thinc-8.3.10-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9306e62c7e7066c63b0c0ba1d164ae0c23bf38edf5a7df2e09cce69a2c290500", size = 3834983, upload-time = "2025-11-17T17:21:36.81Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/a1/d28b21cab9b79e9c803671bebd14489e14c5226136fad6a1c44f96f8e4ef/thinc-8.3.10-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2982604c21096de1a87b04a781a645863eece71ec6ee9f139ac01b998fb5622d", size = 3845215, upload-time = "2025-11-17T17:21:38.362Z" },
+ { url = "https://files.pythonhosted.org/packages/93/9d/ff64ead5f1c2298d9e6a9ccc1c676b2347ac06162ad3c5e5d895c32a719e/thinc-8.3.10-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6b82698e27846004d4eafc38317ace482eced888d4445f7fb9c548fd36777af", size = 4826596, upload-time = "2025-11-17T17:21:40.027Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/44/b80c863608d0fd31641a2d50658560c22d4841f1e445529201e22b3e1d0f/thinc-8.3.10-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2950acab8ae77427a86d11655ed0a161bc83a1edf9d31ba5c43deca6cd27ed4f", size = 4988146, upload-time = "2025-11-17T17:21:41.73Z" },
+ { url = "https://files.pythonhosted.org/packages/93/6d/1bdd9344b2e7299faa55129dda624d50c334eed16a3761eb8b1dacd8bfcd/thinc-8.3.10-cp314-cp314-win_amd64.whl", hash = "sha256:c253139a5c873edf75a3b17ec9d8b6caebee072fdb489594bc64e35115df7625", size = 1738054, upload-time = "2025-11-17T17:21:43.328Z" },
+ { url = "https://files.pythonhosted.org/packages/45/c4/44e3163d48e398efb3748481656963ac6265c14288012871c921dc81d004/thinc-8.3.10-cp314-cp314-win_arm64.whl", hash = "sha256:ad6da67f534995d6ec257f16665377d7ad95bef5c1b1c89618fd4528657a6f24", size = 1665001, upload-time = "2025-11-17T17:21:45.019Z" },
+]
+
+[[package]]
+name = "threadpoolctl"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" }
+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.12.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "regex" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" },
+ { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" },
+ { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" },
+ { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" },
+ { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" },
+ { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" },
+ { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" },
+ { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" },
+ { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" },
+ { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" },
+ { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" },
+ { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" },
+ { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" },
+ { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" },
+ { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" },
+ { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" },
+]
+
+[[package]]
+name = "tokenizers"
+version = "0.22.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "huggingface-hub" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
+ { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
+ { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
+ { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
+ { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
+ { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
+ { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
+ { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
+]
+
+[[package]]
+name = "tomlkit"
+version = "0.12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/9b/42f93f459cf03062c8b3aab812475f01456fd42e04b08bad69bcaedd15c8/tomlkit-0.12.0.tar.gz", hash = "sha256:01f0477981119c7d8ee0f67ebe0297a7c95b14cf9f4b102b45486deb77018716", size = 190497, upload-time = "2023-07-27T07:49:05.797Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/4f/12207897848a653d03ebbf6775a29d949408ded5f99b2d87198bc5c93508/tomlkit-0.12.0-py3-none-any.whl", hash = "sha256:926f1f37a1587c7a4f6c7484dae538f1345d96d793d9adab5d3675957b1d0766", size = 37334, upload-time = "2023-07-27T07:49:04.789Z" },
+]
+
+[[package]]
+name = "torch"
+version = "2.10.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "setuptools" },
+ { name = "sympy" },
+ { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "typing-extensions" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" },
+ { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" },
+ { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" },
+ { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" },
+ { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" },
+ { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" },
+ { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" },
+ { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+]
+
+[[package]]
+name = "transformers"
+version = "4.57.6"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "huggingface-hub" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pyyaml" },
+ { name = "regex" },
+ { name = "requests" },
+ { name = "safetensors" },
+ { name = "tokenizers" },
+ { name = "tqdm" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" },
+]
+
+[[package]]
+name = "triton"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" },
+ { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" },
+ { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" },
+]
+
+[[package]]
+name = "trl"
+version = "0.27.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "accelerate" },
+ { name = "datasets" },
+ { name = "packaging" },
+ { name = "transformers" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2a/85/e0151f2bc006722c032fad942d442ac3cfe1e25b770fca3a6c50e599a89c/trl-0.27.1.tar.gz", hash = "sha256:9d502626c3ac1d32cdc7d8978c742de31bfc11135b4d15be1d83909632dcb75c", size = 449005, upload-time = "2026-01-24T03:33:56.977Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/49/6b03bdbb26c4f4f962624014fe7ae4ea91834286f4387ad0d3748bf21c6f/trl-0.27.1-py3-none-any.whl", hash = "sha256:641843c8556516c39896113b79c9b0b668236670b3eae3697107117c75cc65eb", size = 532873, upload-time = "2026-01-24T03:33:55.195Z" },
+]
+
+[[package]]
+name = "typer"
+version = "0.21.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
+]
+
+[[package]]
+name = "typer-slim"
+version = "0.21.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+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/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]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
+]
+
+[[package]]
+name = "uc-micro-py"
+version = "1.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.6.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.40.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" },
+]
+
+[[package]]
+name = "wandb"
+version = "0.24.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "gitpython" },
+ { name = "packaging" },
+ { name = "platformdirs" },
+ { name = "protobuf" },
+ { name = "pydantic" },
+ { name = "pyyaml" },
+ { name = "requests" },
+ { name = "sentry-sdk" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/27/7e/aad6e943012ea4d88f3a037f1a5a7c6898263c60fbef8c9cdb95a8ff9fd9/wandb-0.24.0.tar.gz", hash = "sha256:4715a243b3d460b6434b9562e935dfd9dfdf5d6e428cfb4c3e7ce4fd44460ab3", size = 44197947, upload-time = "2026-01-13T22:59:59.767Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5f/8a/efec186dcc5dcf3c806040e3f33e58997878b2d30b87aa02b26f046858b6/wandb-0.24.0-py3-none-macosx_12_0_arm64.whl", hash = "sha256:aa9777398ff4b0f04c41359f7d1b95b5d656cb12c37c63903666799212e50299", size = 21464901, upload-time = "2026-01-13T22:59:31.86Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/84/fadf0d5f1d86c3ba662d2b33a15d2b1f08ff1e4e196c77e455f028b0fda2/wandb-0.24.0-py3-none-macosx_12_0_x86_64.whl", hash = "sha256:0423fbd58c3926949724feae8aab89d20c68846f9f4f596b80f9ffe1fc298130", size = 22697817, upload-time = "2026-01-13T22:59:35.267Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/5f/e3124e68d02b30c62856175ce714e07904730be06eecb00f66bb1a59aacf/wandb-0.24.0-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2b25fc0c123daac97ed32912ac55642c65013cc6e3a898e88ca2d917fc8eadc0", size = 21118798, upload-time = "2026-01-13T22:59:38.453Z" },
+ { url = "https://files.pythonhosted.org/packages/22/a1/8d68a914c030e897c306c876d47c73aa5d9ca72be608971290d3a5749570/wandb-0.24.0-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:9485344b4667944b5b77294185bae8469cfa4074869bec0e74f54f8492234cc2", size = 22849954, upload-time = "2026-01-13T22:59:41.265Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/f8/3e68841a4282a4fb6a8935534e6064acc6c9708e8fb76953ec73bbc72a5e/wandb-0.24.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:51b2b9a9d7d6b35640f12a46a48814fd4516807ad44f586b819ed6560f8de1fd", size = 21160339, upload-time = "2026-01-13T22:59:43.967Z" },
+ { url = "https://files.pythonhosted.org/packages/16/e5/d851868ce5b4b437a7cc90405979cd83809790e4e2a2f1e454f63f116e52/wandb-0.24.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:11f7e7841f31eff82c82a677988889ad3aa684c6de61ff82145333b5214ec860", size = 22936978, upload-time = "2026-01-13T22:59:46.911Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/34/43b7f18870051047ce6fe18e7eb24ba7ebdc71663a8f1c58e31e855eb8ac/wandb-0.24.0-py3-none-win32.whl", hash = "sha256:42af348998b00d4309ae790c5374040ac6cc353ab21567f4e29c98c9376dee8e", size = 22118243, upload-time = "2026-01-13T22:59:49.555Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/92/909c81173cf1399111f57f9ca5399a8f165607b024e406e080178c878f70/wandb-0.24.0-py3-none-win_amd64.whl", hash = "sha256:32604eddcd362e1ed4a2e2ce5f3a239369c4a193af223f3e66603481ac91f336", size = 22118246, upload-time = "2026-01-13T22:59:52.126Z" },
+ { url = "https://files.pythonhosted.org/packages/87/85/a845aefd9c2285f98261fa6ffa0a14466366c1ac106d35bc84b654c0ad7f/wandb-0.24.0-py3-none-win_arm64.whl", hash = "sha256:e0f2367552abfca21b0f3a03405fbf48f1e14de9846e70f73c6af5da57afd8ef", size = 20077678, upload-time = "2026-01-13T22:59:56.112Z" },
+]
+
+[[package]]
+name = "wasabi"
+version = "1.1.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ac/f9/054e6e2f1071e963b5e746b48d1e3727470b2a490834d18ad92364929db3/wasabi-1.1.3.tar.gz", hash = "sha256:4bb3008f003809db0c3e28b4daf20906ea871a2bb43f9914197d540f4f2e0878", size = 30391, upload-time = "2024-05-31T16:56:18.99Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/06/7c/34330a89da55610daa5f245ddce5aab81244321101614751e7537f125133/wasabi-1.1.3-py3-none-any.whl", hash = "sha256:f76e16e8f7e79f8c4c8be49b4024ac725713ab10cd7f19350ad18a8e3f71728c", size = 27880, upload-time = "2024-05-31T16:56:16.699Z" },
+]
+
+[[package]]
+name = "weasel"
+version = "0.4.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cloudpathlib" },
+ { name = "confection" },
+ { name = "packaging" },
+ { name = "pydantic" },
+ { name = "requests" },
+ { name = "smart-open" },
+ { name = "srsly" },
+ { name = "typer-slim" },
+ { name = "wasabi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/09/d7/edd9c24e60cf8e5de130aa2e8af3b01521f4d0216c371d01212f580d0d8e/weasel-0.4.3.tar.gz", hash = "sha256:f293d6174398e8f478c78481e00c503ee4b82ea7a3e6d0d6a01e46a6b1396845", size = 38733, upload-time = "2025-11-13T23:52:28.193Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a4/74/a148b41572656904a39dfcfed3f84dd1066014eed94e209223ae8e9d088d/weasel-0.4.3-py3-none-any.whl", hash = "sha256:08f65b5d0dbded4879e08a64882de9b9514753d9eaa4c4e2a576e33666ac12cf", size = 50757, upload-time = "2025-11-13T23:52:26.982Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "12.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2e/62/7a7874b7285413c954a4cca3c11fd851f11b2fe5b4ae2d9bee4f6d9bdb10/websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b", size = 104994, upload-time = "2023-10-21T14:21:11.88Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/6d/23cc898647c8a614a0d9ca703695dd04322fb5135096a20c2684b7c852b6/websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df", size = 124061, upload-time = "2023-10-21T14:20:02.221Z" },
+ { url = "https://files.pythonhosted.org/packages/39/34/364f30fdf1a375e4002a26ee3061138d1571dfda6421126127d379d13930/websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc", size = 121296, upload-time = "2023-10-21T14:20:03.591Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/00/96ae1c9dcb3bc316ef683f2febd8c97dde9f254dc36c3afc65c7645f734c/websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b", size = 121326, upload-time = "2023-10-21T14:20:04.956Z" },
+ { url = "https://files.pythonhosted.org/packages/af/f1/bba1e64430685dd456c1a1fd6b0c791ae33104967b928aefeff261761e8d/websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb", size = 131807, upload-time = "2023-10-21T14:20:06.153Z" },
+ { url = "https://files.pythonhosted.org/packages/62/3b/98ee269712f37d892b93852ce07b3e6d7653160ca4c0d4f8c8663f8021f8/websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92", size = 130751, upload-time = "2023-10-21T14:20:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/00/d6f01ca2b191f8b0808e4132ccd2e7691f0453cbd7d0f72330eb97453c3a/websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed", size = 131176, upload-time = "2023-10-21T14:20:09.212Z" },
+ { url = "https://files.pythonhosted.org/packages/af/9c/703ff3cd8109dcdee6152bae055d852ebaa7750117760ded697ab836cbcf/websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5", size = 136246, upload-time = "2023-10-21T14:20:10.423Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/a5/1a38fb85a456b9dc874ec984f3ff34f6550eafd17a3da28753cd3c1628e8/websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2", size = 135466, upload-time = "2023-10-21T14:20:11.826Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/98/1261f289dff7e65a38d59d2f591de6ed0a2580b729aebddec033c4d10881/websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113", size = 136083, upload-time = "2023-10-21T14:20:13.451Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/1c/f68769fba63ccb9c13fe0a25b616bd5aebeef1c7ddebc2ccc32462fb784d/websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d", size = 124460, upload-time = "2023-10-21T14:20:14.719Z" },
+ { url = "https://files.pythonhosted.org/packages/20/52/8915f51f9aaef4e4361c89dd6cf69f72a0159f14e0d25026c81b6ad22525/websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f", size = 124985, upload-time = "2023-10-21T14:20:15.817Z" },
+ { url = "https://files.pythonhosted.org/packages/79/4d/9cc401e7b07e80532ebc8c8e993f42541534da9e9249c59ee0139dcb0352/websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e", size = 118370, upload-time = "2023-10-21T14:21:10.075Z" },
+]
+
+[[package]]
+name = "werkzeug"
+version = "3.1.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markupsafe" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
+]
+
+[[package]]
+name = "wrapt"
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" },
+ { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" },
+ { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" },
+ { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" },
+ { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" },
+ { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" },
+ { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" },
+ { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" },
+ { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" },
+ { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" },
+ { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" },
+ { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" },
+ { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" },
+ { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" },
+ { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" },
+ { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" },
+ { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" },
+ { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" },
+ { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" },
+ { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" },
+]
+
+[[package]]
+name = "xxhash"
+version = "3.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" },
+ { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" },
+ { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" },
+ { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" },
+ { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" },
+ { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" },
+ { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" },
+ { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" },
+ { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" },
+ { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" },
+ { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" },
+ { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" },
+ { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" },
+ { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" },
+ { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" },
+ { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" },
+ { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" },
+ { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" },
+ { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" },
+ { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" },
+ { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" },
+ { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" },
+ { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" },
+ { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" },
+ { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" },
+ { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" },
+]
+
+[[package]]
+name = "yarl"
+version = "1.22.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "idna" },
+ { name = "multidict" },
+ { name = "propcache" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169, upload-time = "2025-10-06T14:12:55.963Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000, upload-time = "2025-10-06T14:09:44.631Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338, upload-time = "2025-10-06T14:09:46.372Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909, upload-time = "2025-10-06T14:09:48.648Z" },
+ { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940, upload-time = "2025-10-06T14:09:50.089Z" },
+ { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825, upload-time = "2025-10-06T14:09:52.142Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705, upload-time = "2025-10-06T14:09:54.128Z" },
+ { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518, upload-time = "2025-10-06T14:09:55.762Z" },
+ { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267, upload-time = "2025-10-06T14:09:57.958Z" },
+ { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797, upload-time = "2025-10-06T14:09:59.527Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535, upload-time = "2025-10-06T14:10:01.139Z" },
+ { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324, upload-time = "2025-10-06T14:10:02.756Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803, upload-time = "2025-10-06T14:10:04.552Z" },
+ { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220, upload-time = "2025-10-06T14:10:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589, upload-time = "2025-10-06T14:10:09.254Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213, upload-time = "2025-10-06T14:10:11.369Z" },
+ { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330, upload-time = "2025-10-06T14:10:13.112Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980, upload-time = "2025-10-06T14:10:14.601Z" },
+ { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424, upload-time = "2025-10-06T14:10:16.115Z" },
+ { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821, upload-time = "2025-10-06T14:10:17.993Z" },
+ { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243, upload-time = "2025-10-06T14:10:19.44Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361, upload-time = "2025-10-06T14:10:21.124Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036, upload-time = "2025-10-06T14:10:22.902Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671, upload-time = "2025-10-06T14:10:24.523Z" },
+ { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059, upload-time = "2025-10-06T14:10:26.406Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356, upload-time = "2025-10-06T14:10:28.461Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331, upload-time = "2025-10-06T14:10:30.541Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590, upload-time = "2025-10-06T14:10:33.352Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316, upload-time = "2025-10-06T14:10:35.034Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431, upload-time = "2025-10-06T14:10:37.76Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555, upload-time = "2025-10-06T14:10:39.649Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965, upload-time = "2025-10-06T14:10:41.313Z" },
+ { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205, upload-time = "2025-10-06T14:10:43.167Z" },
+ { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209, upload-time = "2025-10-06T14:10:44.643Z" },
+ { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966, upload-time = "2025-10-06T14:10:46.554Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312, upload-time = "2025-10-06T14:10:48.007Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967, upload-time = "2025-10-06T14:10:49.997Z" },
+ { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949, upload-time = "2025-10-06T14:10:52.004Z" },
+ { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818, upload-time = "2025-10-06T14:10:54.078Z" },
+ { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626, upload-time = "2025-10-06T14:10:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129, upload-time = "2025-10-06T14:10:57.985Z" },
+ { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776, upload-time = "2025-10-06T14:10:59.633Z" },
+ { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879, upload-time = "2025-10-06T14:11:01.454Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996, upload-time = "2025-10-06T14:11:03.452Z" },
+ { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047, upload-time = "2025-10-06T14:11:05.115Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947, upload-time = "2025-10-06T14:11:08.137Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943, upload-time = "2025-10-06T14:11:10.284Z" },
+ { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715, upload-time = "2025-10-06T14:11:11.739Z" },
+ { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857, upload-time = "2025-10-06T14:11:13.586Z" },
+ { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520, upload-time = "2025-10-06T14:11:15.465Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504, upload-time = "2025-10-06T14:11:17.106Z" },
+ { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282, upload-time = "2025-10-06T14:11:19.064Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080, upload-time = "2025-10-06T14:11:20.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696, upload-time = "2025-10-06T14:11:22.847Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121, upload-time = "2025-10-06T14:11:24.889Z" },
+ { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080, upload-time = "2025-10-06T14:11:27.307Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661, upload-time = "2025-10-06T14:11:29.387Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645, upload-time = "2025-10-06T14:11:31.423Z" },
+ { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361, upload-time = "2025-10-06T14:11:33.055Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451, upload-time = "2025-10-06T14:11:35.136Z" },
+ { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814, upload-time = "2025-10-06T14:11:37.094Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799, upload-time = "2025-10-06T14:11:38.83Z" },
+ { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990, upload-time = "2025-10-06T14:11:40.624Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292, upload-time = "2025-10-06T14:11:42.578Z" },
+ { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888, upload-time = "2025-10-06T14:11:44.863Z" },
+ { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223, upload-time = "2025-10-06T14:11:46.796Z" },
+ { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981, upload-time = "2025-10-06T14:11:48.845Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303, upload-time = "2025-10-06T14:11:50.897Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820, upload-time = "2025-10-06T14:11:52.549Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203, upload-time = "2025-10-06T14:11:54.225Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173, upload-time = "2025-10-06T14:11:56.069Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562, upload-time = "2025-10-06T14:11:58.783Z" },
+ { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828, upload-time = "2025-10-06T14:12:00.686Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551, upload-time = "2025-10-06T14:12:02.628Z" },
+ { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512, upload-time = "2025-10-06T14:12:04.871Z" },
+ { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400, upload-time = "2025-10-06T14:12:06.624Z" },
+ { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140, upload-time = "2025-10-06T14:12:08.362Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473, upload-time = "2025-10-06T14:12:10.994Z" },
+ { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056, upload-time = "2025-10-06T14:12:13.317Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292, upload-time = "2025-10-06T14:12:15.398Z" },
+ { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
+ { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
+]