feat: remove Zep Cloud, add local SQLite graph store, English-first defaults & Ollama support

- Remove Zep Cloud dependency entirely (zep_entity_reader, zep_graph_memory_updater, zep_tools, zep_paging)
- Add local SQLite-based graph store with LLM-powered entity extraction
- Add local embeddings utility and graph tools service
- Translate all backend docstrings, LLM prompts, and UI to English
- Set English as default language, remove Chinese locale (zh.json)
- Add Ollama setup guide and configure .env.example for Ollama
- Add AGENTS.md for AI agent context
- Fix pyright type errors in models/services
- Add Nedbank Africa expansion preset
- Clean up Docker configuration and README
This commit is contained in:
shayswrld 2026-07-22 21:40:39 +02:00
parent d64175943e
commit 6701212d15
86 changed files with 14907 additions and 16436 deletions

View File

@ -1,16 +1,31 @@
# LLM API配置支持 OpenAI SDK 格式的任意 LLM API
# 推荐使用阿里百炼平台qwen-plus模型https://bailian.console.aliyun.com/
# 注意消耗较大可先进行小于40轮的模拟尝试
LLM_API_KEY=your_api_key_here
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
LLM_MODEL_NAME=qwen-plus
# ===== LLM Configuration =====
# Supports any OpenAI-compatible API. Three options:
# ===== ZEP记忆图谱配置 =====
# 每月免费额度即可支撑简单使用https://app.getzep.com/
ZEP_API_KEY=your_zep_api_key_here
# --- Option A: Ollama Cloud (recommended) ---
# 1. Create an API key: https://ollama.com/settings/keys
# 2. Pick a cloud model from: https://ollama.com/search?c=cloud
LLM_API_KEY=your_ollama_api_key_here
LLM_BASE_URL=https://ollama.com/v1
LLM_MODEL_NAME=qwen3.5:397b
# ===== 加速 LLM 配置(可选)=====
# 注意如果不使用加速配置env文件中就不要出现下面的配置项
LLM_BOOST_API_KEY=your_api_key_here
LLM_BOOST_BASE_URL=your_base_url_here
LLM_BOOST_MODEL_NAME=your_model_name_here
# --- Option B: Local Ollama ---
# 1. Install Ollama: https://ollama.com
# 2. Pull a model: ollama pull qwen2.5:7b
# LLM_API_KEY=ollama
# LLM_BASE_URL=http://localhost:11434/v1
# LLM_MODEL_NAME=qwen2.5:7b
# --- Option C: Other cloud providers (OpenAI, DeepSeek, Alibaba Bailian, etc.) ---
# LLM_API_KEY=your_cloud_api_key
# LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# LLM_MODEL_NAME=qwen-plus
# ===== Optional: Boost LLM (faster/cheaper model for high-volume agent calls) =====
# If you don't use this, DELETE these lines entirely — leaving placeholders breaks things.
# LLM_BOOST_API_KEY=
# LLM_BOOST_BASE_URL=
# LLM_BOOST_MODEL_NAME=
# ===== Optional: Flask / Simulation config =====
# FLASK_DEBUG=True
# OASIS_DEFAULT_MAX_ROUNDS=10

2
.gitignore vendored
View File

@ -2,7 +2,7 @@
.DS_Store
Thumbs.db
# 环境变量(保护敏感信息)
# Environment variables (protect sensitive info)
.env
.env.local
.env.*.local

176
AGENTS.md Normal file
View File

@ -0,0 +1,176 @@
# AGENTS.md
Guide for coding agents operating in this repository. MiroFish is a multi-agent
AI prediction engine: Flask backend (`backend/`) + Vue 3 frontend (`frontend/`)
+ shared i18n (`locales/`). Monorepo orchestrated by root `package.json` via
`concurrently`. Code comments/docstrings are **English**; identifiers
are English. License: AGPL-3.0.
## Build / Run / Test Commands
All commands run from repo root unless noted. Node >=18, Python >=3.11 <=3.12,
`uv` for Python packages.
```bash
# Install everything (root + frontend npm + backend uv venv)
npm run setup:all
# Dev (both services, concurrent, --kill-others)
npm run dev
npm run backend # cd backend && uv run python run.py (Flask :5001)
npm run frontend # cd frontend && npm run dev (Vite :3000)
# Production build (frontend only)
npm run build
# Backend deps only
npm run setup:backend # cd backend && uv sync
# Docker
docker compose up -d # reads root .env, ports 3000+5001, mounts backend/uploads
```
### Tests
**There are no automated tests.** `pytest` + `pytest-asyncio` are declared in
`backend/pyproject.toml` but unused — no `tests/` dir, no `conftest.py`, no
`[tool.pytest.ini_options]`. Frontend has no vitest/jest.
If you add tests, run them with:
```bash
cd backend && uv run pytest # all
cd backend && uv run pytest path/to/test_file.py # single file
cd backend && uv run pytest path/to/test_file.py::test_name # single test
```
`backend/scripts/test_profile_format.py` is a **manual print-based script**, not
a pytest test — run it with `uv run python scripts/test_profile_format.py`.
### Lint / Format
**None configured.** No ruff/black/flake8/mypy/isort on the backend; no
eslint/prettier on the frontend. No CI lint workflow (only Docker image build).
Match the existing de-facto style described below; do not add a linter unless
asked.
## Backend Conventions (Python / Flask)
**Package layout:** `backend/app/` with `api/` (blueprints), `models/`
(dataclasses), `services/` (business logic), `utils/` (logger, retry, llm_client,
locale). Entry point `backend/run.py`. Each `__init__.py` is a manifest: docstring
+ relative imports + `__all__`.
**Imports:** Relative for intra-package (`from .config import Config`,
`from ..services.x import Y`). Absolute `from app...` only in `run.py` and
`scripts/` (which shim `sys.path`). Group: stdlib → third-party → local, blank-line
separated. Late imports inside functions are acceptable for optional/heavy deps.
**Naming:** `snake_case` modules/functions, `PascalCase` classes, `UPPER_SNAKE`
constants, `_`-prefixed private helpers. Blueprints: `snake_case_bp`
(`graph_bp`, `simulation_bp`, `report_bp`). String IDs prefixed
(`proj_`, `sim_`, `mirofish_`).
**Models:** `@dataclass` + `(str, Enum)` for status enums. Hand-written
`to_dict()` / `from_dict()` on every model. **No app-defined Pydantic models**
pydantic is only used in generated ontology code templates and the OASIS SDK.
**Type hints:** Annotate function signatures (`typing.Dict/Any/List/Optional/
Callable/Tuple`). Locals usually unannotated. Be consistent with neighbors.
**Errors:** No custom exception classes; raise `ValueError` for bad state. Every
Flask route wraps in `try/except Exception as e:` returning the uniform envelope:
```python
except Exception as e:
return jsonify({"success": False, "error": str(e),
"traceback": traceback.format_exc()}), 500
```
Success: `{"success": True, "data": {...}}` (often + `"count"`, `"message"`).
`ValueError` → 400/404. Background threads catch exceptions and flip the
`TaskManager`/`Project` status to `FAILED`.
**Logging:** `from ..utils.logger import get_logger` then
`logger = get_logger('mirofish.<area>')` at module top (areas: `api`,
`api.simulation`, `build`, `request`, `retry`, `simulation`, etc.).
f-string messages. Do **not** use `logging.getLogger(__name__)`
`ontology_generator.py` does this and is the one known inconsistency. Rotating
file handler writes to `backend/logs/<date>.log`.
**Async:** Flask is sync (`threaded=True`). Long work goes to
`threading.Thread(target=..., daemon=True)`. **Background threads must re-call
`set_locale()`** — locale is thread-local; capture `get_locale()` before spawning
and restore it inside the thread. Retry helpers in `utils/retry.py`:
`retry_with_backoff` (sync), `retry_with_backoff_async` (async),
`RetryableAPIClient`.
**Config:** `app/config.py` `Config` class loads root `.env` via
`dotenv.load_dotenv('../../.env')` (relative to `config.py`). Required:
`LLM_API_KEY`. Optional: `LLM_BASE_URL`, `LLM_MODEL_NAME`,
`LLM_BOOST_*`, `FLASK_HOST/PORT/DEBUG`, `OASIS_DEFAULT_MAX_ROUNDS`,
`REPORT_AGENT_*`, `SECRET_KEY`. `Config.validate()` is called in `run.py` before
serving. Frontend env: `VITE_API_BASE_URL` (default `http://localhost:5001`).
**Docstrings:** Module docstring (English, one line) at top of every `.py`.
Functions/classes: English summary, Google-style `Args:`/`Returns:`. Flask routes
embed request/response JSON shapes as literal blocks — treat these as the API docs.
**Windows:** `run.py` applies a UTF-8 stdout fix before other imports. Keep that
block first.
## Frontend Conventions (Vue 3 + Vite, plain JS)
**SFCs:** `<script setup>` Composition API only. No Options API, no
`defineComponent({})`. `defineProps({...object syntax...})`,
`defineEmits([...])`. Use `ref`, `reactive`, `computed`, `watch`, `useRouter()`,
`useI18n()`.
**Style:** No semicolons. 2-space indent. Single quotes. Trailing commas in
multi-line objects/arrays. Plain JS (no TypeScript).
**Imports:** Vue ecosystem first (`vue`, `vue-router`, `vue-i18n`), then local
modules. Relative paths dominate (`../api/simulation`); `@``src` and
`@locales` → root `locales/` aliases exist in `vite.config.js` and may be used.
**Naming:** `PascalCase.vue` files (views suffixed `*View.vue`; step components
`Step1..Step5`). `camelCase` JS vars/functions/refs. `kebab-case` template tags
and CSS classes. Route names `PascalCase`.
**State:** No Pinia/Vuex. `store/` holds `reactive()` singletons exporting
setter/getter/clear functions. Component-local state via `ref`/`reactive`.
**API:** Single `axios.create()` instance in `src/api/index.js` (`baseURL` from
`VITE_API_BASE_URL || 'http://localhost:5001'`, 5min timeout). Request interceptor
injects `Accept-Language` from `i18n.global.locale.value`; response interceptor
checks the `success` envelope and rejects on failure. `requestWithRetry(fn,
maxRetries=3, delay=1000)` with exponential backoff. Per-resource modules
(`graph.js`, `simulation.js`, `report.js`) export named functions. JSDoc
`@param`/`@returns` on each.
**i18n:** `vue-i18n` `legacy: false`, `useI18n()`, `$t()`, `<i18n-t>` for rich
inline text. Messages live in **root `locales/*.json`** (`en.json`, `zh.json`),
shared with the backend's `utils/locale.py`. Persisted locale in `localStorage`
(key `locale`, default `zh`).
**CSS:** Plain CSS, no preprocessor. `<style scoped>` default on components;
`App.vue` has unscoped global resets. Global font: `'JetBrains Mono', 'Space
Grotesk', 'Noto Sans SC', monospace`. Palette: black/white + orange
(`#FF4500`/`#FF5722`).
## Ports / Proxy
Backend `5001`, frontend dev `3000`. Vite proxies `/api``http://localhost:5001`
(`changeOrigin`, `secure: false`). Note: `api/index.js` defaults to an absolute
`baseURL`, so set `VITE_API_BASE_URL=''` to use the proxy.
## Known Inconsistencies (fix when touching the file)
- Frontend mixes `export function foo()` and `export const foo = () => {}`.
- `traceback.format_exc()` is leaked in 500 responses (debug aid; not prod-safe).
- `scripts/test_profile_format.py` is named like a pytest test but is a manual
print-based script.
## Agent Workflow Notes
- Match the style of neighboring files; this repo has no linter to enforce.
- Do not add dependencies (Python or JS) for what a few lines can do.
- Keep the `{success, data}` / `{success, error}` envelope on every new route.
- Re-set locale in any new background thread.
- Don't commit `.env`; the root `.env` is gitignored and holds live keys.

View File

@ -1,29 +1,29 @@
FROM python:3.11
# 安装 Node.js (满足 >=18及必要工具
# Install Node.js (>=18) and required tools
RUN apt-get update \
&& apt-get install -y --no-install-recommends nodejs npm \
&& rm -rf /var/lib/apt/lists/*
# 从 uv 官方镜像复制 uv
# Copy uv from official uv image
COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/
WORKDIR /app
# 先复制依赖描述文件以利用缓存
# Copy dependency descriptor files first to leverage caching
COPY package.json package-lock.json ./
COPY frontend/package.json frontend/package-lock.json ./frontend/
COPY backend/pyproject.toml backend/uv.lock ./backend/
# 安装依赖Node + Python
# Install dependencies (Node + Python)
RUN npm ci \
&& npm ci --prefix frontend \
&& cd backend && uv sync --frozen
# 复制项目源码
# Copy project source code
COPY . .
EXPOSE 3000 5001
# 同时启动前后端(开发模式)
# Start both frontend and backend (dev mode)
CMD ["npm", "run", "dev"]

View File

@ -1,203 +0,0 @@
<div align="center">
<img src="./static/image/MiroFish_logo_compressed.jpeg" alt="MiroFish Logo" width="75%"/>
<a href="https://trendshift.io/repositories/16144" target="_blank"><img src="https://trendshift.io/api/badge/repositories/16144" alt="666ghj%2FMiroFish | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
简洁通用的群体智能引擎,预测万物
</br>
<em>A Simple and Universal Swarm Intelligence Engine, Predicting Anything</em>
<a href="https://www.shanda.com/" target="_blank"><img src="./static/image/shanda_logo.png" alt="666ghj%2FMiroFish | Shanda" height="40"/></a>
[![GitHub Stars](https://img.shields.io/github/stars/666ghj/MiroFish?style=flat-square&color=DAA520)](https://github.com/666ghj/MiroFish/stargazers)
[![GitHub Watchers](https://img.shields.io/github/watchers/666ghj/MiroFish?style=flat-square)](https://github.com/666ghj/MiroFish/watchers)
[![GitHub Forks](https://img.shields.io/github/forks/666ghj/MiroFish?style=flat-square)](https://github.com/666ghj/MiroFish/network)
[![Docker](https://img.shields.io/badge/Docker-Build-2496ED?style=flat-square&logo=docker&logoColor=white)](https://hub.docker.com/)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/666ghj/MiroFish)
[![Discord](https://img.shields.io/badge/Discord-Join-5865F2?style=flat-square&logo=discord&logoColor=white)](http://discord.gg/ePf5aPaHnA)
[![X](https://img.shields.io/badge/X-Follow-000000?style=flat-square&logo=x&logoColor=white)](https://x.com/mirofish_ai)
[![Instagram](https://img.shields.io/badge/Instagram-Follow-E4405F?style=flat-square&logo=instagram&logoColor=white)](https://www.instagram.com/mirofish_ai/)
[English](./README.md) | [中文文档](./README-ZH.md)
</div>
## ⚡ 项目概述
**MiroFish** 是一款基于多智能体技术的新一代 AI 预测引擎。通过提取现实世界的种子信息(如突发新闻、政策草案、金融信号),自动构建出高保真的平行数字世界。在此空间内,成千上万个具备独立人格、长期记忆与行为逻辑的智能体进行自由交互与社会演化。你可透过「上帝视角」动态注入变量,精准推演未来走向——**让未来在数字沙盘中预演,助决策在百战模拟后胜出**。
> 你只需:上传种子材料(数据分析报告或者有趣的小说故事),并用自然语言描述预测需求</br>
> MiroFish 将返回:一份详尽的预测报告,以及一个可深度交互的高保真数字世界
### 我们的愿景
MiroFish 致力于打造映射现实的群体智能镜像,通过捕捉个体互动引发的群体涌现,突破传统预测的局限:
- **于宏观**:我们是决策者的预演实验室,让政策与公关在零风险中试错
- **于微观**:我们是个人用户的创意沙盘,无论是推演小说结局还是探索脑洞,皆可有趣、好玩、触手可及
从严肃预测到趣味仿真,我们让每一个如果都能看见结果,让预测万物成为可能。
## 🌐 在线体验
欢迎访问在线 Demo 演示环境,体验我们为你准备的一次关于热点舆情事件的推演预测:[mirofish-live-demo](https://666ghj.github.io/mirofish-demo/)
## 📸 系统截图
<div align="center">
<table>
<tr>
<td><img src="./static/image/Screenshot/运行截图1.png" alt="截图1" width="100%"/></td>
<td><img src="./static/image/Screenshot/运行截图2.png" alt="截图2" width="100%"/></td>
</tr>
<tr>
<td><img src="./static/image/Screenshot/运行截图3.png" alt="截图3" width="100%"/></td>
<td><img src="./static/image/Screenshot/运行截图4.png" alt="截图4" width="100%"/></td>
</tr>
<tr>
<td><img src="./static/image/Screenshot/运行截图5.png" alt="截图5" width="100%"/></td>
<td><img src="./static/image/Screenshot/运行截图6.png" alt="截图6" width="100%"/></td>
</tr>
</table>
</div>
## 🎬 演示视频
### 1. 武汉大学舆情推演预测 + MiroFish项目讲解
<div align="center">
<a href="https://www.bilibili.com/video/BV1VYBsBHEMY/" target="_blank"><img src="./static/image/武大模拟演示封面.png" alt="MiroFish Demo Video" width="75%"/></a>
点击图片查看使用微舆BettaFish生成的《武大舆情报告》进行预测的完整演示视频
</div>
### 2. 《红楼梦》失传结局推演预测
<div align="center">
<a href="https://www.bilibili.com/video/BV1cPk3BBExq" target="_blank"><img src="./static/image/红楼梦模拟推演封面.jpg" alt="MiroFish Demo Video" width="75%"/></a>
点击图片查看基于《红楼梦》前80回数十万字MiroFish深度预测失传结局
</div>
> **金融方向推演预测**、**时政要闻推演预测**等示例陆续更新中...
## 🔄 工作流程
1. **图谱构建**:现实种子提取 & 个体与群体记忆注入 & GraphRAG构建
2. **环境搭建**:实体关系抽取 & 人设生成 & 环境配置Agent注入仿真参数
3. **开始模拟**:双平台并行模拟 & 自动解析预测需求 & 动态更新时序记忆
4. **报告生成**ReportAgent拥有丰富的工具集与模拟后环境进行深度交互
5. **深度互动**:与模拟世界中的任意一位进行对话 & 与ReportAgent进行对话
## 🚀 快速开始
### 一、源码部署(推荐)
#### 前置要求
| 工具 | 版本要求 | 说明 | 安装检查 |
|------|---------|------|---------|
| **Node.js** | 18+ | 前端运行环境,包含 npm | `node -v` |
| **Python** | ≥3.11, ≤3.12 | 后端运行环境 | `python --version` |
| **uv** | 最新版 | Python 包管理器 | `uv --version` |
#### 1. 配置环境变量
```bash
# 复制示例配置文件
cp .env.example .env
# 编辑 .env 文件,填入必要的 API 密钥
```
**必需的环境变量:**
```env
# LLM API配置支持 OpenAI SDK 格式的任意 LLM API
# 推荐使用阿里百炼平台qwen-plus模型https://bailian.console.aliyun.com/
# 注意消耗较大可先进行小于40轮的模拟尝试
LLM_API_KEY=your_api_key
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
LLM_MODEL_NAME=qwen-plus
# Zep Cloud 配置
# 每月免费额度即可支撑简单使用https://app.getzep.com/
ZEP_API_KEY=your_zep_api_key
```
#### 2. 安装依赖
```bash
# 一键安装所有依赖(根目录 + 前端 + 后端)
npm run setup:all
```
或者分步安装:
```bash
# 安装 Node 依赖(根目录 + 前端)
npm run setup
# 安装 Python 依赖(后端,自动创建虚拟环境)
npm run setup:backend
```
#### 3. 启动服务
```bash
# 同时启动前后端(在项目根目录执行)
npm run dev
```
**服务地址:**
- 前端:`http://localhost:3000`
- 后端 API`http://localhost:5001`
**单独启动:**
```bash
npm run backend # 仅启动后端
npm run frontend # 仅启动前端
```
### 二、Docker 部署
```bash
# 1. 配置环境变量(同源码部署)
cp .env.example .env
# 2. 拉取镜像并启动
docker compose up -d
```
默认会读取根目录下的 `.env`,并映射端口 `3000前端/5001后端`
> 在 `docker-compose.yml` 中已通过注释提供加速镜像地址,可按需替换
## 📬 更多交流
<div align="center">
<img src="./static/image/QQ群.png" alt="QQ交流群" width="60%"/>
</div>
&nbsp;
MiroFish团队长期招募全职/实习如果你对多Agent应用感兴趣欢迎投递简历至**mirofish@shanda.com**
## 📄 致谢
**MiroFish 得到了盛大集团的战略支持和孵化!**
MiroFish 的仿真引擎由 **[OASIS](https://github.com/camel-ai/oasis)** 驱动,我们衷心感谢 CAMEL-AI 团队的开源贡献!
## 📈 项目统计
<a href="https://github.com/666ghj/MiroFish">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="static/image/star-history-dark.svg" />
<source media="(prefers-color-scheme: light)" srcset="static/image/star-history-light.svg" />
<img alt="666ghj/MiroFish Star History Chart" src="static/image/star-history-light.svg" />
</picture>
</a>

View File

@ -4,7 +4,7 @@
<a href="https://trendshift.io/repositories/16144" target="_blank"><img src="https://trendshift.io/api/badge/repositories/16144" alt="666ghj%2FMiroFish | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
简洁通用的群体智能引擎,预测万物
A Simple and Universal Swarm Intelligence Engine, Predicting Anything
</br>
<em>A Simple and Universal Swarm Intelligence Engine, Predicting Anything</em>
@ -20,7 +20,7 @@
[![X](https://img.shields.io/badge/X-Follow-000000?style=flat-square&logo=x&logoColor=white)](https://x.com/mirofish_ai)
[![Instagram](https://img.shields.io/badge/Instagram-Follow-E4405F?style=flat-square&logo=instagram&logoColor=white)](https://www.instagram.com/mirofish_ai/)
[English](./README.md) | [中文文档](./README-ZH.md)
[English](./README.md)
</div>
@ -49,16 +49,16 @@ Welcome to visit our online demo environment and experience a prediction simulat
<div align="center">
<table>
<tr>
<td><img src="./static/image/Screenshot/运行截图1.png" alt="Screenshot 1" width="100%"/></td>
<td><img src="./static/image/Screenshot/运行截图2.png" alt="Screenshot 2" width="100%"/></td>
<td><img src="./static/image/Screenshot/screenshot1.png" alt="Screenshot 1" width="100%"/></td>
<td><img src="./static/image/Screenshot/screenshot2.png" alt="Screenshot 2" width="100%"/></td>
</tr>
<tr>
<td><img src="./static/image/Screenshot/运行截图3.png" alt="Screenshot 3" width="100%"/></td>
<td><img src="./static/image/Screenshot/运行截图4.png" alt="Screenshot 4" width="100%"/></td>
<td><img src="./static/image/Screenshot/screenshot3.png" alt="Screenshot 3" width="100%"/></td>
<td><img src="./static/image/Screenshot/screenshot4.png" alt="Screenshot 4" width="100%"/></td>
</tr>
<tr>
<td><img src="./static/image/Screenshot/运行截图5.png" alt="Screenshot 5" width="100%"/></td>
<td><img src="./static/image/Screenshot/运行截图6.png" alt="Screenshot 6" width="100%"/></td>
<td><img src="./static/image/Screenshot/screenshot5.png" alt="Screenshot 5" width="100%"/></td>
<td><img src="./static/image/Screenshot/screenshot6.png" alt="Screenshot 6" width="100%"/></td>
</tr>
</table>
</div>
@ -68,7 +68,7 @@ Welcome to visit our online demo environment and experience a prediction simulat
### 1. Wuhan University Public Opinion Simulation + MiroFish Project Introduction
<div align="center">
<a href="https://www.bilibili.com/video/BV1VYBsBHEMY/" target="_blank"><img src="./static/image/武大模拟演示封面.png" alt="MiroFish Demo Video" width="75%"/></a>
<a href="https://www.bilibili.com/video/BV1VYBsBHEMY/" target="_blank"><img src="./static/image/wuda_demo_cover.png" alt="MiroFish Demo Video" width="75%"/></a>
Click the image to watch the complete demo video for prediction using BettaFish-generated "Wuhan University Public Opinion Report"
</div>
@ -76,7 +76,7 @@ Click the image to watch the complete demo video for prediction using BettaFish-
### 2. Dream of the Red Chamber Lost Ending Simulation
<div align="center">
<a href="https://www.bilibili.com/video/BV1cPk3BBExq" target="_blank"><img src="./static/image/红楼梦模拟推演封面.jpg" alt="MiroFish Demo Video" width="75%"/></a>
<a href="https://www.bilibili.com/video/BV1cPk3BBExq" target="_blank"><img src="./static/image/red_mansion_demo_cover.jpg" alt="MiroFish Demo Video" width="75%"/></a>
Click the image to watch MiroFish's deep prediction of the lost ending based on hundreds of thousands of words from the first 80 chapters of "Dream of the Red Chamber"
</div>
@ -114,19 +114,40 @@ cp .env.example .env
**Required Environment Variables:**
The only required key is `LLM_API_KEY`. The project supports any OpenAI-compatible LLM endpoint.
**Option A — Ollama Cloud (recommended):**
```env
# LLM API Configuration (supports any LLM API with OpenAI SDK format)
# Recommended: Alibaba Qwen-plus model via Bailian Platform: https://bailian.console.aliyun.com/
# High consumption, try simulations with fewer than 40 rounds first
LLM_API_KEY=your_api_key
# 1. Create an API key: https://ollama.com/settings/keys
# 2. Pick a cloud model from: https://ollama.com/search?c=cloud
LLM_API_KEY=your_ollama_api_key
LLM_BASE_URL=https://ollama.com/v1
LLM_MODEL_NAME=qwen3.5:397b
```
> **Model requirements**: The model must support `response_format={"type":"json_object"}` (JSON mode) and tool calling. Recommended cloud models: `qwen3.5:397b`, `glm-5.2`, `deepseek-v4-flash`, `gpt-oss:120b`.
**Option B — Local Ollama:**
```env
# 1. Install Ollama: https://ollama.com
# 2. Pull a model: ollama pull qwen2.5:7b
LLM_API_KEY=ollama
LLM_BASE_URL=http://localhost:11434/v1
LLM_MODEL_NAME=qwen2.5:7b
```
**Option C — Other cloud providers (OpenAI, DeepSeek, etc.):**
```env
LLM_API_KEY=your_cloud_api_key
LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
LLM_MODEL_NAME=qwen-plus
# Zep Cloud Configuration
# Free monthly quota is sufficient for simple usage: https://app.getzep.com/
ZEP_API_KEY=your_zep_api_key
```
> **No external SaaS required**: Knowledge graph storage, entity extraction, and semantic search are all handled locally via sqlite + LLM-based extraction. There is no Zep Cloud dependency.
#### 2. Install Dependencies
```bash
@ -179,7 +200,7 @@ Reads `.env` from root directory by default, maps ports `3000 (frontend) / 5001
## 📬 Join the Conversation
<div align="center">
<img src="./static/image/QQ群.png" alt="QQ Group" width="60%"/>
<img src="./static/image/qq_group.png" alt="QQ Group" width="60%"/>
</div>
&nbsp;

View File

@ -1,12 +1,12 @@
"""
MiroFish Backend - Flask应用工厂
MiroFish Backend - Flask application factory
"""
import os
import warnings
# 抑制 multiprocessing resource_tracker 的警告(来自第三方库如 transformers
# 需要在所有其他导入之前设置
# Suppress multiprocessing resource_tracker warnings (from third-party libraries like transformers)
# Must be set before all other imports
warnings.filterwarnings("ignore", message=".*resource_tracker.*")
from flask import Flask, request
@ -17,64 +17,65 @@ from .utils.logger import setup_logger, get_logger
def create_app(config_class=Config):
"""Flask应用工厂函数"""
"""Flask application factory function"""
app = Flask(__name__)
app.config.from_object(config_class)
# 设置JSON编码确保中文直接显示而不是 \uXXXX 格式)
# Flask >= 2.3 使用 app.json.ensure_ascii旧版本使用 JSON_AS_ASCII 配置
if hasattr(app, 'json') and hasattr(app.json, 'ensure_ascii'):
# Set JSON encoding: ensure Chinese characters display directly (instead of \uXXXX format)
# Flask >= 2.3 uses app.json.ensure_ascii, older versions use JSON_AS_ASCII config
if hasattr(app, "json") and hasattr(app.json, "ensure_ascii"):
app.json.ensure_ascii = False
# 设置日志
logger = setup_logger('mirofish')
# 只在 reloader 子进程中打印启动信息(避免 debug 模式下打印两次)
is_reloader_process = os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
debug_mode = app.config.get('DEBUG', False)
# Set up logging
logger = setup_logger("mirofish")
# Only log startup info in the reloader child process (avoid duplicate logs in debug mode)
is_reloader_process = os.environ.get("WERKZEUG_RUN_MAIN") == "true"
debug_mode = app.config.get("DEBUG", False)
should_log_startup = not debug_mode or is_reloader_process
if should_log_startup:
logger.info("=" * 50)
logger.info("MiroFish Backend 启动中...")
logger.info("MiroFish Backend starting...")
logger.info("=" * 50)
# 启用CORS
# Enable CORS
CORS(app, resources={r"/api/*": {"origins": "*"}})
# 注册模拟进程清理函数(确保服务器关闭时终止所有模拟进程)
# Register simulation process cleanup function (ensure all simulation processes terminate on server shutdown)
from .services.simulation_runner import SimulationRunner
SimulationRunner.register_cleanup()
if should_log_startup:
logger.info("已注册模拟进程清理函数")
# 请求日志中间件
logger.info("Registered simulation process cleanup function")
# Request logging middleware
@app.before_request
def log_request():
logger = get_logger('mirofish.request')
logger.debug(f"请求: {request.method} {request.path}")
if request.content_type and 'json' in request.content_type:
logger.debug(f"请求体: {request.get_json(silent=True)}")
logger = get_logger("mirofish.request")
logger.debug(f"Request: {request.method} {request.path}")
if request.content_type and "json" in request.content_type:
logger.debug(f"Request body: {request.get_json(silent=True)}")
@app.after_request
def log_response(response):
logger = get_logger('mirofish.request')
logger.debug(f"响应: {response.status_code}")
logger = get_logger("mirofish.request")
logger.debug(f"Response: {response.status_code}")
return response
# 注册蓝图
from .api import graph_bp, simulation_bp, report_bp
app.register_blueprint(graph_bp, url_prefix='/api/graph')
app.register_blueprint(simulation_bp, url_prefix='/api/simulation')
app.register_blueprint(report_bp, url_prefix='/api/report')
# 健康检查
@app.route('/health')
def health():
return {'status': 'ok', 'service': 'MiroFish Backend'}
if should_log_startup:
logger.info("MiroFish Backend 启动完成")
return app
# Register blueprints
from .api import graph_bp, simulation_bp, report_bp
app.register_blueprint(graph_bp, url_prefix="/api/graph")
app.register_blueprint(simulation_bp, url_prefix="/api/simulation")
app.register_blueprint(report_bp, url_prefix="/api/report")
# Health check
@app.route("/health")
def health():
return {"status": "ok", "service": "MiroFish Backend"}
if should_log_startup:
logger.info("MiroFish Backend startup complete")
return app

View File

@ -1,14 +1,13 @@
"""
API路由模块
API routes module
"""
from flask import Blueprint
graph_bp = Blueprint('graph', __name__)
simulation_bp = Blueprint('simulation', __name__)
report_bp = Blueprint('report', __name__)
graph_bp = Blueprint("graph", __name__)
simulation_bp = Blueprint("simulation", __name__)
report_bp = Blueprint("report", __name__)
from . import graph # noqa: E402, F401
from . import simulation # noqa: E402, F401
from . import report # noqa: E402, F401

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,76 +1,97 @@
"""\n配置管理\n统一从项目根目录的 .env 文件加载配置\n"""
"""
Configuration management
Loads settings from the .env file at the project root
"""
import os
from dotenv import load_dotenv
# 加载项目根目录的 .env 文件
# 路径: MiroFish/.env (相对于 backend/app/config.py)
project_root_env = os.path.join(os.path.dirname(__file__), '../../.env')
# Load the .env file from the project root
# Path: MiroFish/.env (relative to backend/app/config.py)
project_root_env = os.path.join(os.path.dirname(__file__), "../../.env")
if os.path.exists(project_root_env):
load_dotenv(project_root_env, override=True)
else:
# 如果根目录没有 .env尝试加载环境变量用于生产环境
# If no .env in root, try loading from environment variables (for production)
load_dotenv(override=True)
class Config:
"""Flask配置类"""
# Flask配置
SECRET_KEY = os.environ.get('SECRET_KEY', 'mirofish-secret-key')
DEBUG = os.environ.get('FLASK_DEBUG', 'False').lower() == 'true'
# JSON配置 - 禁用ASCII转义让中文直接显示
"""Flask configuration class"""
# Flask config
SECRET_KEY = os.environ.get("SECRET_KEY", "mirofish-secret-key")
DEBUG = os.environ.get("FLASK_DEBUG", "False").lower() == "true"
# JSON config - disable ASCII escaping so non-ASCII characters display directly
JSON_AS_ASCII = False
# LLM配置统一使用OpenAI格式
LLM_API_KEY = os.environ.get('LLM_API_KEY')
LLM_BASE_URL = os.environ.get('LLM_BASE_URL', 'https://api.openai.com/v1')
LLM_MODEL_NAME = os.environ.get('LLM_MODEL_NAME', 'gpt-4o-mini')
# Zep配置
ZEP_API_KEY = os.environ.get('ZEP_API_KEY')
# 文件上传配置
# LLM config (unified OpenAI format)
LLM_API_KEY = os.environ.get("LLM_API_KEY")
LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1")
LLM_MODEL_NAME = os.environ.get("LLM_MODEL_NAME", "gpt-4o-mini")
# File upload config
MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), '../uploads')
ALLOWED_EXTENSIONS = {'pdf', 'md', 'txt', 'markdown'}
# 文本处理配置
DEFAULT_CHUNK_SIZE = 500 # 默认切块大小
DEFAULT_CHUNK_OVERLAP = 50 # 默认重叠大小
# OASIS模拟配置
OASIS_DEFAULT_MAX_ROUNDS = int(os.environ.get('OASIS_DEFAULT_MAX_ROUNDS', '10'))
OASIS_SIMULATION_DATA_DIR = os.path.join(os.path.dirname(__file__), '../uploads/simulations')
# OASIS平台可用动作配置
UPLOAD_FOLDER = os.path.join(os.path.dirname(__file__), "../uploads")
ALLOWED_EXTENSIONS = {"pdf", "md", "txt", "markdown"}
# Text processing config
DEFAULT_CHUNK_SIZE = 500 # default chunk size
DEFAULT_CHUNK_OVERLAP = 50 # default overlap size
# OASIS simulation config
OASIS_DEFAULT_MAX_ROUNDS = int(os.environ.get("OASIS_DEFAULT_MAX_ROUNDS", "10"))
OASIS_SIMULATION_DATA_DIR = os.path.join(
os.path.dirname(__file__), "../uploads/simulations"
)
# OASIS platform available actions config
OASIS_TWITTER_ACTIONS = [
'CREATE_POST', 'LIKE_POST', 'REPOST', 'FOLLOW', 'DO_NOTHING', 'QUOTE_POST'
"CREATE_POST",
"LIKE_POST",
"REPOST",
"FOLLOW",
"DO_NOTHING",
"QUOTE_POST",
]
OASIS_REDDIT_ACTIONS = [
'LIKE_POST', 'DISLIKE_POST', 'CREATE_POST', 'CREATE_COMMENT',
'LIKE_COMMENT', 'DISLIKE_COMMENT', 'SEARCH_POSTS', 'SEARCH_USER',
'TREND', 'REFRESH', 'DO_NOTHING', 'FOLLOW', 'MUTE'
"LIKE_POST",
"DISLIKE_POST",
"CREATE_POST",
"CREATE_COMMENT",
"LIKE_COMMENT",
"DISLIKE_COMMENT",
"SEARCH_POSTS",
"SEARCH_USER",
"TREND",
"REFRESH",
"DO_NOTHING",
"FOLLOW",
"MUTE",
]
# Report Agent配置
REPORT_AGENT_MAX_TOOL_CALLS = int(os.environ.get('REPORT_AGENT_MAX_TOOL_CALLS', '5'))
REPORT_AGENT_MAX_REFLECTION_ROUNDS = int(os.environ.get('REPORT_AGENT_MAX_REFLECTION_ROUNDS', '2'))
REPORT_AGENT_TEMPERATURE = float(os.environ.get('REPORT_AGENT_TEMPERATURE', '0.5'))
# Report Agent config
REPORT_AGENT_MAX_TOOL_CALLS = int(
os.environ.get("REPORT_AGENT_MAX_TOOL_CALLS", "5")
)
REPORT_AGENT_MAX_REFLECTION_ROUNDS = int(
os.environ.get("REPORT_AGENT_MAX_REFLECTION_ROUNDS", "2")
)
REPORT_AGENT_TEMPERATURE = float(os.environ.get("REPORT_AGENT_TEMPERATURE", "0.5"))
@classmethod
def validate(cls) -> list[str]:
"""验证必要配置"""
errors: list[str] = []
def validate(cls):
"""Validate required configuration"""
errors = []
if not cls.LLM_API_KEY:
errors.append("LLM_API_KEY 未配置")
if not cls.ZEP_API_KEY:
errors.append("ZEP_API_KEY 未配置")
if os.environ.get("ZEP_API_URL"):
errors.append("ZEP_API_URL 不受支持MiroFish 仅连接 Zep Cloud")
errors.append("LLM_API_KEY not configured")
if cls.DEBUG:
import warnings
warnings.warn("Flask DEBUG mode is enabled. Do not use in production.", RuntimeWarning)
return errors
warnings.warn(
"Flask DEBUG mode is enabled. Do not use in production.",
RuntimeWarning,
)
return errors

View File

@ -1,9 +1,8 @@
"""
数据模型模块
Data models module
"""
from .task import TaskManager, TaskStatus
from .project import Project, ProjectStatus, ProjectManager
__all__ = ['TaskManager', 'TaskStatus', 'Project', 'ProjectStatus', 'ProjectManager']
__all__ = ["TaskManager", "TaskStatus", "Project", "ProjectStatus", "ProjectManager"]

View File

@ -1,6 +1,6 @@
"""
项目上下文管理
用于在服务端持久化项目状态避免前端在接口间传递大量数据
Project context management
Used to persist project state on the server side, avoiding the frontend passing large amounts of data between interfaces
"""
import os
@ -15,51 +15,55 @@ from ..config import Config
class ProjectStatus(str, Enum):
"""项目状态"""
CREATED = "created" # 刚创建,文件已上传
ONTOLOGY_GENERATED = "ontology_generated" # 本体已生成
GRAPH_BUILDING = "graph_building" # 图谱构建中
GRAPH_COMPLETED = "graph_completed" # 图谱构建完成
FAILED = "failed" # 失败
"""Project status"""
CREATED = "created" # just created, files uploaded
ONTOLOGY_GENERATED = "ontology_generated" # ontology generated
GRAPH_BUILDING = "graph_building" # graph building in progress
GRAPH_COMPLETED = "graph_completed" # graph build completed
FAILED = "failed" # failed
@dataclass
class Project:
"""项目数据模型"""
"""Project data model"""
project_id: str
name: str
status: ProjectStatus
created_at: str
updated_at: str
# 文件信息
files: List[Dict[str, str]] = field(default_factory=list) # [{filename, path, size}]
# File info
files: List[Dict[str, str]] = field(
default_factory=list
) # [{filename, path, size}]
total_text_length: int = 0
# 本体信息接口1生成后填充
# Ontology info (populated after endpoint 1 generates it)
ontology: Optional[Dict[str, Any]] = None
analysis_summary: Optional[str] = None
# 图谱信息接口2完成后填充
# Graph info (populated after endpoint 2 completes)
graph_id: Optional[str] = None
graph_build_task_id: Optional[str] = None
zep_batch_id: Optional[str] = None
zep_batch_operation_id: Optional[str] = None
# 配置
# Config
simulation_requirement: Optional[str] = None
chunk_size: int = 500
chunk_overlap: int = 50
# 错误信息
# Error info
error: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
"""Convert to dictionary"""
return {
"project_id": self.project_id,
"name": self.name,
"status": self.status.value if isinstance(self.status, ProjectStatus) else self.status,
"status": self.status.value
if isinstance(self.status, ProjectStatus)
else self.status,
"created_at": self.created_at,
"updated_at": self.updated_at,
"files": self.files,
@ -68,253 +72,241 @@ class Project:
"analysis_summary": self.analysis_summary,
"graph_id": self.graph_id,
"graph_build_task_id": self.graph_build_task_id,
"zep_batch_id": self.zep_batch_id,
"zep_batch_operation_id": self.zep_batch_operation_id,
"simulation_requirement": self.simulation_requirement,
"chunk_size": self.chunk_size,
"chunk_overlap": self.chunk_overlap,
"error": self.error
"error": self.error,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Project':
"""从字典创建"""
status = data.get('status', 'created')
def from_dict(cls, data: Dict[str, Any]) -> "Project":
"""Create from dictionary"""
status = data.get("status", "created")
if isinstance(status, str):
status = ProjectStatus(status)
return cls(
project_id=data['project_id'],
name=data.get('name', 'Unnamed Project'),
project_id=data["project_id"],
name=data.get("name", "Unnamed Project"),
status=status,
created_at=data.get('created_at', ''),
updated_at=data.get('updated_at', ''),
files=data.get('files', []),
total_text_length=data.get('total_text_length', 0),
ontology=data.get('ontology'),
analysis_summary=data.get('analysis_summary'),
graph_id=data.get('graph_id'),
graph_build_task_id=data.get('graph_build_task_id'),
zep_batch_id=data.get('zep_batch_id'),
zep_batch_operation_id=data.get('zep_batch_operation_id'),
simulation_requirement=data.get('simulation_requirement'),
chunk_size=data.get('chunk_size', 500),
chunk_overlap=data.get('chunk_overlap', 50),
error=data.get('error')
created_at=data.get("created_at", ""),
updated_at=data.get("updated_at", ""),
files=data.get("files", []),
total_text_length=data.get("total_text_length", 0),
ontology=data.get("ontology"),
analysis_summary=data.get("analysis_summary"),
graph_id=data.get("graph_id"),
graph_build_task_id=data.get("graph_build_task_id"),
simulation_requirement=data.get("simulation_requirement"),
chunk_size=data.get("chunk_size", 500),
chunk_overlap=data.get("chunk_overlap", 50),
error=data.get("error"),
)
class ProjectManager:
"""项目管理器 - 负责项目的持久化存储和检索"""
# 项目存储根目录
PROJECTS_DIR = os.path.join(Config.UPLOAD_FOLDER, 'projects')
"""Project manager - responsible for project persistence storage and retrieval"""
# Project storage root directory
PROJECTS_DIR = os.path.join(Config.UPLOAD_FOLDER, "projects")
@classmethod
def _ensure_projects_dir(cls):
"""确保项目目录存在"""
"""Ensure the projects directory exists"""
os.makedirs(cls.PROJECTS_DIR, exist_ok=True)
@classmethod
def _get_project_dir(cls, project_id: str) -> str:
"""获取项目目录路径"""
"""Get the project directory path"""
return os.path.join(cls.PROJECTS_DIR, project_id)
@classmethod
def _get_project_meta_path(cls, project_id: str) -> str:
"""获取项目元数据文件路径"""
return os.path.join(cls._get_project_dir(project_id), 'project.json')
"""Get the project metadata file path"""
return os.path.join(cls._get_project_dir(project_id), "project.json")
@classmethod
def _get_project_files_dir(cls, project_id: str) -> str:
"""获取项目文件存储目录"""
return os.path.join(cls._get_project_dir(project_id), 'files')
"""Get the project file storage directory"""
return os.path.join(cls._get_project_dir(project_id), "files")
@classmethod
def _get_project_text_path(cls, project_id: str) -> str:
"""获取项目提取文本存储路径"""
return os.path.join(cls._get_project_dir(project_id), 'extracted_text.txt')
"""Get the path where the project's extracted text is stored"""
return os.path.join(cls._get_project_dir(project_id), "extracted_text.txt")
@classmethod
def create_project(cls, name: str = "Unnamed Project") -> Project:
"""
创建新项目
Create a new project
Args:
name: 项目名称
name: Project name
Returns:
新创建的Project对象
The newly created Project object
"""
cls._ensure_projects_dir()
project_id = f"proj_{uuid.uuid4().hex[:12]}"
now = datetime.now().isoformat()
project = Project(
project_id=project_id,
name=name,
status=ProjectStatus.CREATED,
created_at=now,
updated_at=now
updated_at=now,
)
# 创建项目目录结构
# Create project directory structure
project_dir = cls._get_project_dir(project_id)
files_dir = cls._get_project_files_dir(project_id)
os.makedirs(project_dir, exist_ok=True)
os.makedirs(files_dir, exist_ok=True)
# 保存项目元数据
# Save project metadata
cls.save_project(project)
return project
@classmethod
def save_project(cls, project: Project) -> None:
"""保存项目元数据"""
"""Save project metadata"""
project.updated_at = datetime.now().isoformat()
meta_path = cls._get_project_meta_path(project.project_id)
with open(meta_path, 'w', encoding='utf-8') as f:
with open(meta_path, "w", encoding="utf-8") as f:
json.dump(project.to_dict(), f, ensure_ascii=False, indent=2)
@classmethod
def get_project(cls, project_id: str) -> Optional[Project]:
"""
获取项目
Get a project
Args:
project_id: 项目ID
project_id: Project ID
Returns:
Project对象如果不存在返回None
Project object, or None if it does not exist
"""
meta_path = cls._get_project_meta_path(project_id)
if not os.path.exists(meta_path):
return None
with open(meta_path, 'r', encoding='utf-8') as f:
with open(meta_path, "r", encoding="utf-8") as f:
data = json.load(f)
return Project.from_dict(data)
@classmethod
def list_projects(cls, limit: Optional[int] = 50) -> List[Project]:
def list_projects(cls, limit: int = 50) -> List[Project]:
"""
列出所有项目
List all projects
Args:
limit: 返回数量限制
limit: Return count limit
Returns:
项目列表按创建时间倒序
List of projects, sorted by creation time in descending order
"""
cls._ensure_projects_dir()
projects = []
for project_id in os.listdir(cls.PROJECTS_DIR):
project = cls.get_project(project_id)
if project:
projects.append(project)
# 按创建时间倒序排序
# Sort by creation time in descending order
projects.sort(key=lambda p: p.created_at, reverse=True)
return projects if limit is None else projects[:limit]
@classmethod
def find_projects_by_graph_id(cls, graph_id: str) -> List[Project]:
"""Return every persisted project that references a Cloud graph."""
return projects[:limit]
return [
project
for project in cls.list_projects(limit=None)
if project.graph_id == graph_id
]
@classmethod
def delete_project(cls, project_id: str) -> bool:
"""
删除项目及其所有文件
Delete a project and all its files
Args:
project_id: 项目ID
project_id: Project ID
Returns:
是否删除成功
Whether the deletion was successful
"""
project_dir = cls._get_project_dir(project_id)
if not os.path.exists(project_dir):
return False
shutil.rmtree(project_dir)
return True
@classmethod
def save_file_to_project(cls, project_id: str, file_storage, original_filename: str) -> Dict[str, str]:
def save_file_to_project(
cls, project_id: str, file_storage, original_filename: str
) -> Dict[str, Any]:
"""
保存上传的文件到项目目录
Save an uploaded file to the project directory
Args:
project_id: 项目ID
file_storage: Flask的FileStorage对象
original_filename: 原始文件名
project_id: Project ID
file_storage: Flask FileStorage object
original_filename: Original filename
Returns:
文件信息字典 {filename, path, size}
File info dictionary {filename, path, size}
"""
files_dir = cls._get_project_files_dir(project_id)
os.makedirs(files_dir, exist_ok=True)
# 生成安全的文件名
# Generate a safe filename
ext = os.path.splitext(original_filename)[1].lower()
safe_filename = f"{uuid.uuid4().hex[:8]}{ext}"
file_path = os.path.join(files_dir, safe_filename)
# 保存文件
# Save file
file_storage.save(file_path)
# 获取文件大小
# Get file size
file_size = os.path.getsize(file_path)
return {
"original_filename": original_filename,
"saved_filename": safe_filename,
"path": file_path,
"size": file_size
"size": file_size,
}
@classmethod
def save_extracted_text(cls, project_id: str, text: str) -> None:
"""保存提取的文本"""
"""Save the extracted text"""
text_path = cls._get_project_text_path(project_id)
with open(text_path, 'w', encoding='utf-8') as f:
with open(text_path, "w", encoding="utf-8") as f:
f.write(text)
@classmethod
def get_extracted_text(cls, project_id: str) -> Optional[str]:
"""获取提取的文本"""
"""Get the extracted text"""
text_path = cls._get_project_text_path(project_id)
if not os.path.exists(text_path):
return None
with open(text_path, 'r', encoding='utf-8') as f:
with open(text_path, "r", encoding="utf-8") as f:
return f.read()
@classmethod
def get_project_files(cls, project_id: str) -> List[str]:
"""获取项目的所有文件路径"""
"""Get all file paths of the project"""
files_dir = cls._get_project_files_dir(project_id)
if not os.path.exists(files_dir):
return []
return [
os.path.join(files_dir, f)
for f in os.listdir(files_dir)
os.path.join(files_dir, f)
for f in os.listdir(files_dir)
if os.path.isfile(os.path.join(files_dir, f))
]

View File

@ -1,6 +1,6 @@
"""
任务状态管理
用于跟踪长时间运行的任务如图谱构建
Task status management
Used to track long-running tasks (such as graph building)
"""
import uuid
@ -14,30 +14,32 @@ from ..utils.locale import t
class TaskStatus(str, Enum):
"""任务状态枚举"""
PENDING = "pending" # 等待中
PROCESSING = "processing" # 处理中
COMPLETED = "completed" # 已完成
FAILED = "failed" # 失败
"""Task status enum"""
PENDING = "pending" # pending
PROCESSING = "processing" # processing
COMPLETED = "completed" # completed
FAILED = "failed" # failed
@dataclass
class Task:
"""任务数据类"""
"""Task data class"""
task_id: str
task_type: str
status: TaskStatus
created_at: datetime
updated_at: datetime
progress: int = 0 # 总进度百分比 0-100
message: str = "" # 状态消息
result: Optional[Dict] = None # 任务结果
error: Optional[str] = None # 错误信息
metadata: Dict = field(default_factory=dict) # 额外元数据
progress_detail: Dict = field(default_factory=dict) # 详细进度信息
progress: int = 0 # overall progress percentage 0-100
message: str = "" # status message
result: Optional[Dict] = None # task result
error: Optional[str] = None # error message
metadata: Dict = field(default_factory=dict) # additional metadata
progress_detail: Dict = field(default_factory=dict) # detailed progress info
def to_dict(self) -> Dict[str, Any]:
"""转换为字典"""
"""Convert to dict"""
return {
"task_id": self.task_id,
"task_type": self.task_type,
@ -55,56 +57,58 @@ class Task:
class TaskManager:
"""
任务管理器
线程安全的任务状态管理
Task manager
Thread-safe task state management
"""
_instance = None
_lock = threading.Lock()
_tasks: Dict[str, "Task"]
_task_lock: threading.Lock
def __new__(cls):
"""单例模式"""
"""Singleton pattern"""
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance._tasks: Dict[str, Task] = {}
cls._instance._tasks = {}
cls._instance._task_lock = threading.Lock()
return cls._instance
def create_task(self, task_type: str, metadata: Optional[Dict] = None) -> str:
"""
创建新任务
Create a new task
Args:
task_type: 任务类型
metadata: 额外元数据
task_type: Task type
metadata: Additional metadata
Returns:
任务ID
Task ID
"""
task_id = str(uuid.uuid4())
now = datetime.now()
task = Task(
task_id=task_id,
task_type=task_type,
status=TaskStatus.PENDING,
created_at=now,
updated_at=now,
metadata=metadata or {}
metadata=metadata or {},
)
with self._task_lock:
self._tasks[task_id] = task
return task_id
def get_task(self, task_id: str) -> Optional[Task]:
"""获取任务"""
"""Get a task"""
with self._task_lock:
return self._tasks.get(task_id)
def update_task(
self,
task_id: str,
@ -113,19 +117,19 @@ class TaskManager:
message: Optional[str] = None,
result: Optional[Dict] = None,
error: Optional[str] = None,
progress_detail: Optional[Dict] = None
progress_detail: Optional[Dict] = None,
):
"""
更新任务状态
Update task status
Args:
task_id: 任务ID
status: 新状态
progress: 进度
message: 消息
result: 结果
error: 错误信息
progress_detail: 详细进度信息
task_id: Task ID
status: New status
progress: Progress
message: Message
result: Result
error: Error message
progress_detail: Detailed progress info
"""
with self._task_lock:
task = self._tasks.get(task_id)
@ -143,44 +147,49 @@ class TaskManager:
task.error = error
if progress_detail is not None:
task.progress_detail = progress_detail
def complete_task(self, task_id: str, result: Dict):
"""标记任务完成"""
"""Mark a task as completed"""
self.update_task(
task_id,
status=TaskStatus.COMPLETED,
progress=100,
message=t('progress.taskComplete'),
result=result
message=t("progress.taskComplete"),
result=result,
)
def fail_task(self, task_id: str, error: str):
"""标记任务失败"""
"""Mark a task as failed"""
self.update_task(
task_id,
status=TaskStatus.FAILED,
message=t('progress.taskFailed'),
error=error
message=t("progress.taskFailed"),
error=error,
)
def list_tasks(self, task_type: Optional[str] = None) -> list:
"""列出任务"""
"""List tasks"""
with self._task_lock:
tasks = list(self._tasks.values())
if task_type:
tasks = [t for t in tasks if t.task_type == task_type]
return [t.to_dict() for t in sorted(tasks, key=lambda x: x.created_at, reverse=True)]
return [
t.to_dict()
for t in sorted(tasks, key=lambda x: x.created_at, reverse=True)
]
def cleanup_old_tasks(self, max_age_hours: int = 24):
"""清理旧任务"""
"""Clean up old tasks"""
from datetime import timedelta
cutoff = datetime.now() - timedelta(hours=max_age_hours)
with self._task_lock:
old_ids = [
tid for tid, task in self._tasks.items()
if task.created_at < cutoff and task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED]
tid
for tid, task in self._tasks.items()
if task.created_at < cutoff
and task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED]
]
for tid in old_ids:
del self._tasks[tid]

View File

@ -1,32 +1,27 @@
"""
业务服务模块
Business services module
"""
from .ontology_generator import OntologyGenerator
from .graph_builder import GraphBuilderService
from .text_processor import TextProcessor
from .zep_entity_reader import ZepEntityReader, EntityNode, FilteredEntities
from .entity_reader import EntityReader, EntityNode, FilteredEntities
from .oasis_profile_generator import OasisProfileGenerator, OasisAgentProfile
from .simulation_manager import SimulationManager, SimulationState, SimulationStatus
from .simulation_config_generator import (
SimulationConfigGenerator,
SimulationConfigGenerator,
SimulationParameters,
AgentActivityConfig,
TimeSimulationConfig,
EventConfig,
PlatformConfig
PlatformConfig,
)
from .simulation_runner import (
SimulationRunner,
SimulationRunState,
RunnerStatus,
AgentAction,
RoundSummary
)
from .zep_graph_memory_updater import (
ZepGraphMemoryUpdater,
ZepGraphMemoryManager,
AgentActivity
RoundSummary,
)
from .simulation_ipc import (
SimulationIPCClient,
@ -34,40 +29,36 @@ from .simulation_ipc import (
IPCCommand,
IPCResponse,
CommandType,
CommandStatus
CommandStatus,
)
__all__ = [
'OntologyGenerator',
'GraphBuilderService',
'TextProcessor',
'ZepEntityReader',
'EntityNode',
'FilteredEntities',
'OasisProfileGenerator',
'OasisAgentProfile',
'SimulationManager',
'SimulationState',
'SimulationStatus',
'SimulationConfigGenerator',
'SimulationParameters',
'AgentActivityConfig',
'TimeSimulationConfig',
'EventConfig',
'PlatformConfig',
'SimulationRunner',
'SimulationRunState',
'RunnerStatus',
'AgentAction',
'RoundSummary',
'ZepGraphMemoryUpdater',
'ZepGraphMemoryManager',
'AgentActivity',
'SimulationIPCClient',
'SimulationIPCServer',
'IPCCommand',
'IPCResponse',
'CommandType',
'CommandStatus',
"OntologyGenerator",
"GraphBuilderService",
"TextProcessor",
"EntityReader",
"EntityNode",
"FilteredEntities",
"OasisProfileGenerator",
"OasisAgentProfile",
"SimulationManager",
"SimulationState",
"SimulationStatus",
"SimulationConfigGenerator",
"SimulationParameters",
"AgentActivityConfig",
"TimeSimulationConfig",
"EventConfig",
"PlatformConfig",
"SimulationRunner",
"SimulationRunState",
"RunnerStatus",
"AgentAction",
"RoundSummary",
"SimulationIPCClient",
"SimulationIPCServer",
"IPCCommand",
"IPCResponse",
"CommandType",
"CommandStatus",
]

View File

@ -0,0 +1,292 @@
"""
Entity reader reads and filters entities from local sqlite graph store.
Replaces the former Zep-based entity reader (now uses local SQLite).
"""
from typing import Dict, Any, List, Optional, Set
from dataclasses import dataclass, field
from ..utils.logger import get_logger
from .local_graph_store import LocalGraphStore
logger = get_logger("mirofish.entity_reader")
@dataclass
class EntityNode:
"""Entity node data structure"""
uuid: str
name: str
labels: List[str]
summary: str
attributes: Dict[str, Any]
related_edges: List[Dict[str, Any]] = field(default_factory=list)
related_nodes: List[Dict[str, Any]] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"uuid": self.uuid,
"name": self.name,
"labels": self.labels,
"summary": self.summary,
"attributes": self.attributes,
"related_edges": self.related_edges,
"related_nodes": self.related_nodes,
}
def get_entity_type(self) -> Optional[str]:
"""Get entity type (excluding default Entity label)"""
for label in self.labels:
if label not in ["Entity", "Node"]:
return label
return None
@dataclass
class FilteredEntities:
"""Filtered entity set"""
entities: List[EntityNode]
entity_types: Set[str]
total_count: int
filtered_count: int
def to_dict(self) -> Dict[str, Any]:
return {
"entities": [e.to_dict() for e in self.entities],
"entity_types": list(self.entity_types),
"total_count": self.total_count,
"filtered_count": self.filtered_count,
}
class EntityReader:
"""
Entity reader and filter service
Reads nodes from the local graph store, filters by defined entity types.
Kept class name for import compatibility.
"""
def __init__(self, api_key: Optional[str] = None):
pass
def get_all_nodes(self, graph_id: str) -> List[Dict[str, Any]]:
"""Get all nodes from graph"""
logger.info(f"Reading all nodes from graph {graph_id}...")
store = LocalGraphStore(graph_id)
nodes = store.get_all_nodes()
nodes_data = [n.to_dict() for n in nodes]
logger.info(f"Got {len(nodes_data)} nodes")
return nodes_data
def get_all_edges(self, graph_id: str) -> List[Dict[str, Any]]:
"""Get all edges from graph"""
logger.info(f"Reading all edges from graph {graph_id}...")
store = LocalGraphStore(graph_id)
edges = store.get_all_edges()
edges_data = []
for edge in edges:
edges_data.append(
{
"uuid": edge.uuid,
"name": edge.name,
"fact": edge.fact,
"source_node_uuid": edge.source_node_uuid,
"target_node_uuid": edge.target_node_uuid,
"attributes": edge.attributes,
}
)
logger.info(f"Got {len(edges_data)} edges")
return edges_data
def get_node_edges(self, graph_id: str, node_uuid: str) -> List[Dict[str, Any]]:
"""Get edges for a specific node"""
store = LocalGraphStore(graph_id)
edges = store.get_node_edges(node_uuid)
edges_data = []
for edge in edges:
edges_data.append(
{
"uuid": edge.uuid,
"name": edge.name,
"fact": edge.fact,
"source_node_uuid": edge.source_node_uuid,
"target_node_uuid": edge.target_node_uuid,
"attributes": edge.attributes,
}
)
return edges_data
def filter_defined_entities(
self,
graph_id: str,
defined_entity_types: Optional[List[str]] = None,
enrich_with_edges: bool = True,
) -> FilteredEntities:
"""Filter nodes matching defined entity types"""
logger.info(f"Filtering entities in graph {graph_id}...")
all_nodes = self.get_all_nodes(graph_id)
total_count = len(all_nodes)
all_edges = self.get_all_edges(graph_id) if enrich_with_edges else []
node_map = {n["uuid"]: n for n in all_nodes}
filtered_entities = []
entity_types_found = set()
for node in all_nodes:
labels = node.get("labels", [])
custom_labels = [l for l in labels if l not in ["Entity", "Node"]]
if not custom_labels:
continue
if defined_entity_types:
matching_labels = [
l for l in custom_labels if l in defined_entity_types
]
if not matching_labels:
continue
entity_type = matching_labels[0]
else:
entity_type = custom_labels[0]
entity_types_found.add(entity_type)
entity = EntityNode(
uuid=node["uuid"],
name=node["name"],
labels=labels,
summary=node["summary"],
attributes=node["attributes"],
)
if enrich_with_edges:
related_edges = []
related_node_uuids = set()
for edge in all_edges:
if edge["source_node_uuid"] == node["uuid"]:
related_edges.append(
{
"direction": "outgoing",
"edge_name": edge["name"],
"fact": edge["fact"],
"target_node_uuid": edge["target_node_uuid"],
}
)
related_node_uuids.add(edge["target_node_uuid"])
elif edge["target_node_uuid"] == node["uuid"]:
related_edges.append(
{
"direction": "incoming",
"edge_name": edge["name"],
"fact": edge["fact"],
"source_node_uuid": edge["source_node_uuid"],
}
)
related_node_uuids.add(edge["source_node_uuid"])
entity.related_edges = related_edges
related_nodes = []
for related_uuid in related_node_uuids:
if related_uuid in node_map:
rn = node_map[related_uuid]
related_nodes.append(
{
"uuid": rn["uuid"],
"name": rn["name"],
"labels": rn["labels"],
"summary": rn.get("summary", ""),
}
)
entity.related_nodes = related_nodes
filtered_entities.append(entity)
logger.info(
f"Filter complete: {total_count} total, {len(filtered_entities)} matched, types: {entity_types_found}"
)
return FilteredEntities(
entities=filtered_entities,
entity_types=entity_types_found,
total_count=total_count,
filtered_count=len(filtered_entities),
)
def get_entity_with_context(
self, graph_id: str, entity_uuid: str
) -> Optional[EntityNode]:
"""Get single entity with full context (edges + related nodes)"""
store = LocalGraphStore(graph_id)
node = store.get_node(entity_uuid)
if not node:
return None
edges = self.get_node_edges(graph_id, entity_uuid)
all_nodes = self.get_all_nodes(graph_id)
node_map = {n["uuid"]: n for n in all_nodes}
related_edges = []
related_node_uuids = set()
for edge in edges:
if edge["source_node_uuid"] == entity_uuid:
related_edges.append(
{
"direction": "outgoing",
"edge_name": edge["name"],
"fact": edge["fact"],
"target_node_uuid": edge["target_node_uuid"],
}
)
related_node_uuids.add(edge["target_node_uuid"])
else:
related_edges.append(
{
"direction": "incoming",
"edge_name": edge["name"],
"fact": edge["fact"],
"source_node_uuid": edge["source_node_uuid"],
}
)
related_node_uuids.add(edge["source_node_uuid"])
related_nodes = []
for related_uuid in related_node_uuids:
if related_uuid in node_map:
rn = node_map[related_uuid]
related_nodes.append(
{
"uuid": rn["uuid"],
"name": rn["name"],
"labels": rn["labels"],
"summary": rn.get("summary", ""),
}
)
return EntityNode(
uuid=node.uuid,
name=node.name,
labels=node.labels,
summary=node.summary,
attributes=node.attributes,
related_edges=related_edges,
related_nodes=related_nodes,
)
def get_entities_by_type(
self, graph_id: str, entity_type: str, enrich_with_edges: bool = True
) -> List[EntityNode]:
"""Get all entities of a specific type"""
result = self.filter_defined_entities(
graph_id=graph_id,
defined_entity_types=[entity_type],
enrich_with_edges=enrich_with_edges,
)
return result.entities

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
"""
Local graph extractor LLM-based entity/relation extraction from text chunks.
Replaces Zep Cloud server-side NLP extraction pipeline (now local LLM-based).
"""
import json
from typing import Dict, Any, List, Optional
from ..utils.llm_client import LLMClient
from ..utils.logger import get_logger
from ..utils.embeddings import embed
from .local_graph_store import LocalGraphStore
logger = get_logger("mirofish.extractor")
EXTRACT_SYSTEM_PROMPT = """You are a knowledge-graph extraction engine. Given a text chunk and an ontology (entity types + relation types), extract entities and relations as valid JSON.
Output ONLY valid JSON in this exact format:
{
"entities": [
{"name": "EntityName", "type": "EntityType", "summary": "One-sentence description", "attributes": {}}
],
"edges": [
{"name": "relation_type", "fact": "Natural language fact statement", "source": "SourceEntityName", "target": "TargetEntityName", "attributes": {}}
]
}
Rules:
1. Only use entity types and relation types defined in the ontology
2. Entity names should be proper nouns or specific identifiers from the text
3. Each edge fact must be a complete sentence describing the relationship
4. If no entities/edges are found, return {"entities": [], "edges": []}
5. Do not include any text outside the JSON object"""
class LocalGraphExtractor:
"""Extracts entities and relations from text using LLM, stores in LocalGraphStore."""
def __init__(self, graph_id: str, ontology: Dict[str, Any]):
self.store = LocalGraphStore(graph_id)
self.ontology = ontology
self.llm = LLMClient()
self._entity_index: Dict[str, str] = {} # name -> uuid
def extract_and_store(self, chunks: List[str], progress_callback=None) -> int:
"""Extract entities/edges from text chunks, store in graph. Returns total extracted count."""
entity_types = [e["name"] for e in self.ontology.get("entity_types", [])]
edge_types = [e["name"] for e in self.ontology.get("edge_types", [])]
ontology_summary = json.dumps(
{"entity_types": entity_types, "edge_types": edge_types}, ensure_ascii=False
)
total = 0
for i, chunk in enumerate(chunks):
if progress_callback:
progress_callback(i + 1, len(chunks))
result = self._extract_from_chunk(chunk, ontology_summary)
if not result:
continue
for entity in result.get("entities", []):
self._add_or_merge_entity(entity)
for edge in result.get("edges", []):
self._add_edge(edge)
total += len(result.get("entities", [])) + len(result.get("edges", []))
return total
def _extract_from_chunk(
self, chunk: str, ontology_summary: str
) -> Optional[Dict[str, Any]]:
user_msg = f"Ontology types: {ontology_summary}\n\nText chunk:\n{chunk}"
try:
messages = [
{"role": "system", "content": EXTRACT_SYSTEM_PROMPT},
{"role": "user", "content": user_msg},
]
return self.llm.chat_json(messages, temperature=0.2, max_tokens=8192)
except Exception as e:
logger.warning(f"Extraction failed for chunk: {str(e)[:100]}")
return None
def _add_or_merge_entity(self, entity: Dict[str, Any]):
name = entity.get("name", "").strip()
etype = entity.get("type", "Entity")
if not name:
return
existing = self.store.find_node_by_name(name, labels=[etype])
if existing:
return
labels = ["Entity", etype] if etype != "Entity" else ["Entity"]
summary = entity.get("summary", "")
attributes = entity.get("attributes", {})
emb = embed(f"{name} {summary}") if summary else embed(name)
node_uuid = self.store.add_node(
name=name,
labels=labels,
summary=summary,
attributes=attributes,
embedding=emb,
)
self._entity_index[f"{name}:{etype}"] = node_uuid
def _add_edge(self, edge: Dict[str, Any]):
name = edge.get("name", "").strip()
source_name = edge.get("source", "").strip()
target_name = edge.get("target", "").strip()
fact = edge.get("fact", "").strip()
if not name or not source_name or not target_name or not fact:
return
source_uuid = self._find_entity_uuid(source_name)
target_uuid = self._find_entity_uuid(target_name)
if not source_uuid or not target_uuid:
return
emb = embed(fact)
self.store.add_edge(
name=name,
fact=fact,
source_node_uuid=source_uuid,
target_node_uuid=target_uuid,
fact_type=name,
attributes=edge.get("attributes", {}),
embedding=emb,
)
def _find_entity_uuid(self, name: str) -> Optional[str]:
for key, uuid in self._entity_index.items():
if key.startswith(f"{name}:"):
return uuid
node = self.store.find_node_by_name(name)
if node:
self._entity_index[
f"{node.name}:{node.labels[-1] if len(node.labels) > 1 else 'Entity'}"
] = node.uuid
return node.uuid
return None

View File

@ -0,0 +1,426 @@
"""
Local graph store sqlite-backed knowledge graph (replaces Zep Cloud).
Stores nodes, edges, episodes, and embeddings per graph.
"""
import os
import json
import sqlite3
import uuid
from typing import Dict, Any, List, Optional
from dataclasses import dataclass, field
from ..utils.logger import get_logger
logger = get_logger("mirofish.graph_store")
GRAPH_DIR = os.path.join(os.path.dirname(__file__), "../../uploads/graphs")
def _ensure_graph_dir(graph_id: str) -> str:
d = os.path.join(GRAPH_DIR, graph_id)
os.makedirs(d, exist_ok=True)
return d
def _db_path(graph_id: str) -> str:
return os.path.join(_ensure_graph_dir(graph_id), "graph.db")
def _connect(graph_id: str) -> sqlite3.Connection:
conn = sqlite3.connect(_db_path(graph_id))
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA journal_mode=WAL")
return conn
SCHEMA = """
CREATE TABLE IF NOT EXISTS graph_meta (
graph_id TEXT PRIMARY KEY,
name TEXT,
description TEXT,
ontology TEXT,
created_at TEXT
);
CREATE TABLE IF NOT EXISTS nodes (
uuid TEXT PRIMARY KEY,
graph_id TEXT,
name TEXT,
labels TEXT,
summary TEXT,
attributes TEXT,
embedding TEXT,
created_at TEXT
);
CREATE TABLE IF NOT EXISTS edges (
uuid TEXT PRIMARY KEY,
graph_id TEXT,
name TEXT,
fact TEXT,
fact_type TEXT,
source_node_uuid TEXT,
target_node_uuid TEXT,
attributes TEXT,
embedding TEXT,
created_at TEXT,
valid_at TEXT,
invalid_at TEXT,
expired_at TEXT,
episodes TEXT
);
CREATE TABLE IF NOT EXISTS episodes (
uuid TEXT PRIMARY KEY,
graph_id TEXT,
data TEXT,
type TEXT,
processed INTEGER DEFAULT 1,
created_at TEXT
);
CREATE INDEX IF NOT EXISTS idx_nodes_graph ON nodes(graph_id);
CREATE INDEX IF NOT EXISTS idx_edges_graph ON edges(graph_id);
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_node_uuid);
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_node_uuid);
"""
@dataclass
class LocalNode:
uuid: str
name: str
labels: List[str]
summary: str
attributes: Dict[str, Any]
created_at: Optional[str] = None
embedding: Optional[List[float]] = None
@property
def uuid_(self) -> str:
return self.uuid
def to_dict(self) -> Dict[str, Any]:
return {
"uuid": self.uuid,
"name": self.name,
"labels": self.labels,
"summary": self.summary,
"attributes": self.attributes,
"created_at": self.created_at,
}
@dataclass
class LocalEdge:
uuid: str
name: str
fact: str
fact_type: str
source_node_uuid: str
target_node_uuid: str
attributes: Dict[str, Any]
created_at: Optional[str] = None
valid_at: Optional[str] = None
invalid_at: Optional[str] = None
expired_at: Optional[str] = None
episodes: Optional[List[str]] = None
embedding: Optional[List[float]] = None
@property
def uuid_(self) -> str:
return self.uuid
def to_dict(self) -> Dict[str, Any]:
return {
"uuid": self.uuid,
"name": self.name,
"fact": self.fact,
"fact_type": self.fact_type,
"source_node_uuid": self.source_node_uuid,
"target_node_uuid": self.target_node_uuid,
"attributes": self.attributes,
"created_at": self.created_at,
"valid_at": self.valid_at,
"invalid_at": self.invalid_at,
"expired_at": self.expired_at,
"episodes": self.episodes or [],
}
class LocalGraphStore:
"""Sqlite-backed graph store. One DB file per graph."""
def __init__(self, graph_id: str):
self.graph_id = graph_id
self._init_schema()
def _init_schema(self):
with _connect(self.graph_id) as conn:
conn.executescript(SCHEMA)
conn.commit()
def set_meta(
self, name: str, description: str, ontology: Dict[str, Any], created_at: str
):
with _connect(self.graph_id) as conn:
conn.execute(
"INSERT OR REPLACE INTO graph_meta (graph_id, name, description, ontology, created_at) VALUES (?, ?, ?, ?, ?)",
(
self.graph_id,
name,
description,
json.dumps(ontology, ensure_ascii=False),
created_at,
),
)
conn.commit()
def get_ontology(self) -> Dict[str, Any]:
with _connect(self.graph_id) as conn:
row = conn.execute(
"SELECT ontology FROM graph_meta WHERE graph_id=?", (self.graph_id,)
).fetchone()
if row:
return json.loads(row["ontology"])
return {}
def add_episode(self, data: str, episode_type: str = "text") -> str:
ep_uuid = str(uuid.uuid4())
from datetime import datetime
with _connect(self.graph_id) as conn:
conn.execute(
"INSERT INTO episodes (uuid, graph_id, data, type, processed, created_at) VALUES (?, ?, ?, ?, 1, ?)",
(
ep_uuid,
self.graph_id,
data,
episode_type,
datetime.now().isoformat(),
),
)
conn.commit()
return ep_uuid
def add_node(
self,
name: str,
labels: List[str],
summary: str = "",
attributes: Optional[Dict[str, Any]] = None,
embedding: Optional[List[float]] = None,
) -> str:
node_uuid = str(uuid.uuid4())
from datetime import datetime
with _connect(self.graph_id) as conn:
conn.execute(
"INSERT OR REPLACE INTO nodes (uuid, graph_id, name, labels, summary, attributes, embedding, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
(
node_uuid,
self.graph_id,
name,
json.dumps(labels, ensure_ascii=False),
summary,
json.dumps(attributes or {}, ensure_ascii=False),
json.dumps(embedding) if embedding else None,
datetime.now().isoformat(),
),
)
conn.commit()
return node_uuid
def add_edge(
self,
name: str,
fact: str,
source_node_uuid: str,
target_node_uuid: str,
fact_type: str = "",
attributes: Optional[Dict[str, Any]] = None,
embedding: Optional[List[float]] = None,
valid_at: Optional[str] = None,
invalid_at: Optional[str] = None,
expired_at: Optional[str] = None,
) -> str:
edge_uuid = str(uuid.uuid4())
from datetime import datetime
with _connect(self.graph_id) as conn:
conn.execute(
"""INSERT OR REPLACE INTO edges
(uuid, graph_id, name, fact, fact_type, source_node_uuid, target_node_uuid,
attributes, embedding, created_at, valid_at, invalid_at, expired_at, episodes)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
edge_uuid,
self.graph_id,
name,
fact,
fact_type or name,
source_node_uuid,
target_node_uuid,
json.dumps(attributes or {}, ensure_ascii=False),
json.dumps(embedding) if embedding else None,
datetime.now().isoformat(),
valid_at,
invalid_at,
expired_at,
"[]",
),
)
conn.commit()
return edge_uuid
def find_node_by_name(
self, name: str, labels: Optional[List[str]] = None
) -> Optional[LocalNode]:
with _connect(self.graph_id) as conn:
if labels:
rows = conn.execute(
"SELECT * FROM nodes WHERE graph_id=? AND name=?",
(self.graph_id, name),
).fetchall()
for row in rows:
node_labels = json.loads(row["labels"])
if any(l in node_labels for l in labels):
return self._row_to_node(row)
if rows:
return self._row_to_node(rows[0])
else:
row = conn.execute(
"SELECT * FROM nodes WHERE graph_id=? AND name=? LIMIT 1",
(self.graph_id, name),
).fetchone()
if row:
return self._row_to_node(row)
return None
def get_node(self, node_uuid: str) -> Optional[LocalNode]:
with _connect(self.graph_id) as conn:
row = conn.execute(
"SELECT * FROM nodes WHERE uuid=?", (node_uuid,)
).fetchone()
if row:
return self._row_to_node(row)
return None
def get_all_nodes(self) -> List[LocalNode]:
with _connect(self.graph_id) as conn:
rows = conn.execute(
"SELECT * FROM nodes WHERE graph_id=?", (self.graph_id,)
).fetchall()
return [self._row_to_node(r) for r in rows]
def get_all_edges(self) -> List[LocalEdge]:
with _connect(self.graph_id) as conn:
rows = conn.execute(
"SELECT * FROM edges WHERE graph_id=?", (self.graph_id,)
).fetchall()
return [self._row_to_edge(r) for r in rows]
def get_node_edges(self, node_uuid: str) -> List[LocalEdge]:
with _connect(self.graph_id) as conn:
rows = conn.execute(
"SELECT * FROM edges WHERE source_node_uuid=? OR target_node_uuid=?",
(node_uuid, node_uuid),
).fetchall()
return [self._row_to_edge(r) for r in rows]
def search_nodes(self, query: str, limit: int = 20) -> List[LocalNode]:
# ponytail: LIKE keyword search; O(n) scan per query, fine for graphs < 10k nodes;
# upgrade to embedding cosine sim via embed() if recall matters
pattern = f"%{query}%"
with _connect(self.graph_id) as conn:
rows = conn.execute(
"SELECT * FROM nodes WHERE graph_id=? AND (name LIKE ? OR summary LIKE ?) LIMIT ?",
(self.graph_id, pattern, pattern, limit),
).fetchall()
return [self._row_to_node(r) for r in rows]
def search_edges(self, query: str, limit: int = 20) -> List[LocalEdge]:
pattern = f"%{query}%"
with _connect(self.graph_id) as conn:
rows = conn.execute(
"SELECT * FROM edges WHERE graph_id=? AND (fact LIKE ? OR name LIKE ?) LIMIT ?",
(self.graph_id, pattern, pattern, limit),
).fetchall()
return [self._row_to_edge(r) for r in rows]
def get_statistics(self) -> Dict[str, Any]:
with _connect(self.graph_id) as conn:
node_count = conn.execute(
"SELECT COUNT(*) FROM nodes WHERE graph_id=?", (self.graph_id,)
).fetchone()[0]
edge_count = conn.execute(
"SELECT COUNT(*) FROM edges WHERE graph_id=?", (self.graph_id,)
).fetchone()[0]
entity_types = set()
for row in conn.execute(
"SELECT labels FROM nodes WHERE graph_id=?", (self.graph_id,)
).fetchall():
labels = json.loads(row["labels"])
for l in labels:
if l not in ("Entity", "Node"):
entity_types.add(l)
return {
"node_count": node_count,
"edge_count": edge_count,
"entity_types": list(entity_types),
}
def delete(self):
import shutil
d = os.path.join(GRAPH_DIR, self.graph_id)
if os.path.exists(d):
shutil.rmtree(d)
def _row_to_node(self, row: sqlite3.Row) -> LocalNode:
embedding = None
if row["embedding"]:
try:
embedding = json.loads(row["embedding"])
except Exception:
pass
return LocalNode(
uuid=row["uuid"],
name=row["name"],
labels=json.loads(row["labels"]) if row["labels"] else [],
summary=row["summary"] or "",
attributes=json.loads(row["attributes"]) if row["attributes"] else {},
created_at=row["created_at"],
embedding=embedding,
)
def _row_to_edge(self, row: sqlite3.Row) -> LocalEdge:
embedding = None
if row["embedding"]:
try:
embedding = json.loads(row["embedding"])
except Exception:
pass
episodes = []
if row["episodes"]:
try:
episodes = json.loads(row["episodes"])
except Exception:
pass
return LocalEdge(
uuid=row["uuid"],
name=row["name"] or "",
fact=row["fact"] or "",
fact_type=row["fact_type"] or "",
source_node_uuid=row["source_node_uuid"],
target_node_uuid=row["target_node_uuid"],
attributes=json.loads(row["attributes"]) if row["attributes"] else {},
created_at=row["created_at"],
valid_at=row["valid_at"],
invalid_at=row["invalid_at"],
expired_at=row["expired_at"],
episodes=episodes,
embedding=embedding,
)
def delete_graph(graph_id: str):
store = LocalGraphStore(graph_id)
store.delete()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
"""
模拟IPC通信模块
用于Flask后端和模拟脚本之间的进程间通信
Simulation IPC communication module
Used for inter-process communication between the Flask backend and the simulation script
通过文件系统实现简单的命令/响应模式
1. Flask写入命令到 commands/ 目录
2. 模拟脚本轮询命令目录执行命令并写入响应到 responses/ 目录
3. Flask轮询响应目录获取结果
Implements a simple command/response pattern via the file system:
1. Flask writes commands to the commands/ directory
2. The simulation script polls the command directory, executes commands, and writes responses to the responses/ directory
3. Flask polls the response directory to get results
"""
import os
@ -18,19 +18,22 @@ from datetime import datetime
from enum import Enum
from ..utils.logger import get_logger
from ..utils.locale import t
logger = get_logger('mirofish.simulation_ipc')
logger = get_logger("mirofish.simulation_ipc")
class CommandType(str, Enum):
"""命令类型"""
INTERVIEW = "interview" # 单个Agent采访
BATCH_INTERVIEW = "batch_interview" # 批量采访
CLOSE_ENV = "close_env" # 关闭环境
"""Command type"""
INTERVIEW = "interview" # Single Agent interview
BATCH_INTERVIEW = "batch_interview" # Batch interview
CLOSE_ENV = "close_env" # Close environment
class CommandStatus(str, Enum):
"""命令状态"""
"""Command status"""
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
@ -39,246 +42,237 @@ class CommandStatus(str, Enum):
@dataclass
class IPCCommand:
"""IPC命令"""
"""IPC command"""
command_id: str
command_type: CommandType
args: Dict[str, Any]
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
def to_dict(self) -> Dict[str, Any]:
return {
"command_id": self.command_id,
"command_type": self.command_type.value,
"args": self.args,
"timestamp": self.timestamp
"timestamp": self.timestamp,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'IPCCommand':
def from_dict(cls, data: Dict[str, Any]) -> "IPCCommand":
return cls(
command_id=data["command_id"],
command_type=CommandType(data["command_type"]),
args=data.get("args", {}),
timestamp=data.get("timestamp", datetime.now().isoformat())
timestamp=data.get("timestamp", datetime.now().isoformat()),
)
@dataclass
class IPCResponse:
"""IPC响应"""
"""IPC response"""
command_id: str
status: CommandStatus
result: Optional[Dict[str, Any]] = None
error: Optional[str] = None
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
def to_dict(self) -> Dict[str, Any]:
return {
"command_id": self.command_id,
"status": self.status.value,
"result": self.result,
"error": self.error,
"timestamp": self.timestamp
"timestamp": self.timestamp,
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'IPCResponse':
def from_dict(cls, data: Dict[str, Any]) -> "IPCResponse":
return cls(
command_id=data["command_id"],
status=CommandStatus(data["status"]),
result=data.get("result"),
error=data.get("error"),
timestamp=data.get("timestamp", datetime.now().isoformat())
timestamp=data.get("timestamp", datetime.now().isoformat()),
)
class SimulationIPCClient:
"""
模拟IPC客户端Flask端使用
用于向模拟进程发送命令并等待响应
Simulation IPC client (used on the Flask side)
Used to send commands to the simulation process and wait for responses
"""
def __init__(self, simulation_dir: str):
"""
初始化IPC客户端
Initialize the IPC client
Args:
simulation_dir: 模拟数据目录
simulation_dir: Simulation data directory
"""
self.simulation_dir = simulation_dir
self.commands_dir = os.path.join(simulation_dir, "ipc_commands")
self.responses_dir = os.path.join(simulation_dir, "ipc_responses")
# 确保目录存在
# Ensure directories exist
os.makedirs(self.commands_dir, exist_ok=True)
os.makedirs(self.responses_dir, exist_ok=True)
def send_command(
self,
command_type: CommandType,
args: Dict[str, Any],
timeout: float = 60.0,
poll_interval: float = 0.5
poll_interval: float = 0.5,
) -> IPCResponse:
"""
发送命令并等待响应
Send a command and wait for a response
Args:
command_type: 命令类型
args: 命令参数
timeout: 超时时间
poll_interval: 轮询间隔
command_type: Command type
args: Command arguments
timeout: Timeout duration (seconds)
poll_interval: Polling interval (seconds)
Returns:
IPCResponse
Raises:
TimeoutError: 等待响应超时
TimeoutError: Timed out waiting for a response
"""
command_id = str(uuid.uuid4())
command = IPCCommand(
command_id=command_id,
command_type=command_type,
args=args
command_id=command_id, command_type=command_type, args=args
)
# 写入命令文件
# Write command file
command_file = os.path.join(self.commands_dir, f"{command_id}.json")
with open(command_file, 'w', encoding='utf-8') as f:
with open(command_file, "w", encoding="utf-8") as f:
json.dump(command.to_dict(), f, ensure_ascii=False, indent=2)
logger.info(f"发送IPC命令: {command_type.value}, command_id={command_id}")
# 等待响应
logger.info(
f"Sending IPC command: {command_type.value}, command_id={command_id}"
)
# Wait for response
response_file = os.path.join(self.responses_dir, f"{command_id}.json")
start_time = time.time()
while time.time() - start_time < timeout:
if os.path.exists(response_file):
try:
with open(response_file, 'r', encoding='utf-8') as f:
with open(response_file, "r", encoding="utf-8") as f:
response_data = json.load(f)
response = IPCResponse.from_dict(response_data)
# 清理命令和响应文件
# Clean up command and response files
try:
os.remove(command_file)
os.remove(response_file)
except OSError:
pass
logger.info(f"收到IPC响应: command_id={command_id}, status={response.status.value}")
logger.info(
f"Received IPC response: command_id={command_id}, status={response.status.value}"
)
return response
except (json.JSONDecodeError, KeyError) as e:
logger.warning(f"解析响应失败: {e}")
logger.warning(f"Failed to parse response: {e}")
time.sleep(poll_interval)
# 超时
logger.error(f"等待IPC响应超时: command_id={command_id}")
# 清理命令文件
# Timeout
logger.error(f"Timed out waiting for IPC response: command_id={command_id}")
# Clean up command file
try:
os.remove(command_file)
except OSError:
pass
raise TimeoutError(f"等待命令响应超时 ({timeout}秒)")
raise TimeoutError(t("api.ipcTimeout", timeout=timeout))
def send_interview(
self,
agent_id: int,
prompt: str,
platform: str = None,
timeout: float = 60.0
self, agent_id: int, prompt: str, platform: str = None, timeout: float = 60.0
) -> IPCResponse:
"""
发送单个Agent采访命令
Send a single-Agent interview command
Args:
agent_id: Agent ID
prompt: 采访问题
platform: 指定平台可选
- "twitter": 只采访Twitter平台
- "reddit": 只采访Reddit平台
- None: 双平台模拟时同时采访两个平台单平台模拟时采访该平台
timeout: 超时时间
prompt: Interview question
platform: Specified platform (optional)
- "twitter": only interview the Twitter platform
- "reddit": only interview the Reddit platform
- None: in dual-platform simulation, interview both platforms simultaneously; in single-platform simulation, interview that platform
timeout: Timeout duration
Returns:
IPCResponseresult字段包含采访结果
IPCResponse, the result field contains the interview result
"""
args = {
"agent_id": agent_id,
"prompt": prompt
}
args = {"agent_id": agent_id, "prompt": prompt}
if platform:
args["platform"] = platform
return self.send_command(
command_type=CommandType.INTERVIEW,
args=args,
timeout=timeout
command_type=CommandType.INTERVIEW, args=args, timeout=timeout
)
def send_batch_interview(
self,
interviews: List[Dict[str, Any]],
platform: str = None,
timeout: float = 120.0
timeout: float = 120.0,
) -> IPCResponse:
"""
发送批量采访命令
Send a batch interview command
Args:
interviews: 采访列表每个元素包含 {"agent_id": int, "prompt": str, "platform": str(可选)}
platform: 默认平台可选会被每个采访项的platform覆盖
- "twitter": 默认只采访Twitter平台
- "reddit": 默认只采访Reddit平台
- None: 双平台模拟时每个Agent同时采访两个平台
timeout: 超时时间
interviews: Interview list, each element contains {"agent_id": int, "prompt": str, "platform": str (optional)}
platform: Default platform (optional, overridden by each interview item's platform)
- "twitter": default to only interview the Twitter platform
- "reddit": default to only interview the Reddit platform
- None: in dual-platform simulation, each Agent is interviewed on both platforms simultaneously
timeout: Timeout duration
Returns:
IPCResponseresult字段包含所有采访结果
IPCResponse, the result field contains all interview results
"""
args = {"interviews": interviews}
if platform:
args["platform"] = platform
return self.send_command(
command_type=CommandType.BATCH_INTERVIEW,
args=args,
timeout=timeout
command_type=CommandType.BATCH_INTERVIEW, args=args, timeout=timeout
)
def send_close_env(self, timeout: float = 30.0) -> IPCResponse:
"""
发送关闭环境命令
Send a close-environment command
Args:
timeout: 超时时间
timeout: Timeout duration
Returns:
IPCResponse
"""
return self.send_command(
command_type=CommandType.CLOSE_ENV,
args={},
timeout=timeout
command_type=CommandType.CLOSE_ENV, args={}, timeout=timeout
)
def check_env_alive(self) -> bool:
"""
检查模拟环境是否存活
通过检查 env_status.json 文件来判断
Check whether the simulation environment is alive
Determined by checking the env_status.json file
"""
status_file = os.path.join(self.simulation_dir, "env_status.json")
if not os.path.exists(status_file):
return False
try:
with open(status_file, 'r', encoding='utf-8') as f:
with open(status_file, "r", encoding="utf-8") as f:
status = json.load(f)
return status.get("status") == "alive"
except (json.JSONDecodeError, OSError):
@ -287,108 +281,108 @@ class SimulationIPCClient:
class SimulationIPCServer:
"""
模拟IPC服务器模拟脚本端使用
轮询命令目录执行命令并返回响应
Simulation IPC server (used on the simulation script side)
Polls the command directory, executes commands, and returns responses
"""
def __init__(self, simulation_dir: str):
"""
初始化IPC服务器
Initialize the IPC server
Args:
simulation_dir: 模拟数据目录
simulation_dir: Simulation data directory
"""
self.simulation_dir = simulation_dir
self.commands_dir = os.path.join(simulation_dir, "ipc_commands")
self.responses_dir = os.path.join(simulation_dir, "ipc_responses")
# 确保目录存在
# Ensure directories exist
os.makedirs(self.commands_dir, exist_ok=True)
os.makedirs(self.responses_dir, exist_ok=True)
# 环境状态
# Environment status
self._running = False
def start(self):
"""标记服务器为运行状态"""
"""Mark the server as running"""
self._running = True
self._update_env_status("alive")
def stop(self):
"""标记服务器为停止状态"""
"""Mark the server as stopped"""
self._running = False
self._update_env_status("stopped")
def _update_env_status(self, status: str):
"""更新环境状态文件"""
"""Update the environment status file"""
status_file = os.path.join(self.simulation_dir, "env_status.json")
with open(status_file, 'w', encoding='utf-8') as f:
json.dump({
"status": status,
"timestamp": datetime.now().isoformat()
}, f, ensure_ascii=False, indent=2)
with open(status_file, "w", encoding="utf-8") as f:
json.dump(
{"status": status, "timestamp": datetime.now().isoformat()},
f,
ensure_ascii=False,
indent=2,
)
def poll_commands(self) -> Optional[IPCCommand]:
"""
轮询命令目录返回第一个待处理的命令
Poll the command directory and return the first pending command
Returns:
IPCCommand None
IPCCommand or None
"""
if not os.path.exists(self.commands_dir):
return None
# 按时间排序获取命令文件
# Sort command files by time
command_files = []
for filename in os.listdir(self.commands_dir):
if filename.endswith('.json'):
if filename.endswith(".json"):
filepath = os.path.join(self.commands_dir, filename)
command_files.append((filepath, os.path.getmtime(filepath)))
command_files.sort(key=lambda x: x[1])
for filepath, _ in command_files:
try:
with open(filepath, 'r', encoding='utf-8') as f:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
return IPCCommand.from_dict(data)
except (json.JSONDecodeError, KeyError, OSError) as e:
logger.warning(f"读取命令文件失败: {filepath}, {e}")
logger.warning(f"Failed to read command file: {filepath}, {e}")
continue
return None
def send_response(self, response: IPCResponse):
"""
发送响应
Send a response
Args:
response: IPC响应
response: IPC response
"""
response_file = os.path.join(self.responses_dir, f"{response.command_id}.json")
with open(response_file, 'w', encoding='utf-8') as f:
with open(response_file, "w", encoding="utf-8") as f:
json.dump(response.to_dict(), f, ensure_ascii=False, indent=2)
# 删除命令文件
# Delete command file
command_file = os.path.join(self.commands_dir, f"{response.command_id}.json")
try:
os.remove(command_file)
except OSError:
pass
def send_success(self, command_id: str, result: Dict[str, Any]):
"""发送成功响应"""
self.send_response(IPCResponse(
command_id=command_id,
status=CommandStatus.COMPLETED,
result=result
))
"""Send a success response"""
self.send_response(
IPCResponse(
command_id=command_id, status=CommandStatus.COMPLETED, result=result
)
)
def send_error(self, command_id: str, error: str):
"""发送错误响应"""
self.send_response(IPCResponse(
command_id=command_id,
status=CommandStatus.FAILED,
error=error
))
"""Send an error response"""
self.send_response(
IPCResponse(command_id=command_id, status=CommandStatus.FAILED, error=error)
)

View File

@ -1,7 +1,7 @@
"""
OASIS模拟管理器
管理Twitter和Reddit双平台并行模拟
使用预设脚本 + LLM智能生成配置参数
OASIS simulation manager
Manages parallel Twitter and Reddit dual-platform simulation
Uses preset scripts + LLM-generated configuration parameters
"""
import os
@ -14,71 +14,72 @@ from enum import Enum
from ..config import Config
from ..utils.logger import get_logger
from .zep_entity_reader import ZepEntityReader, FilteredEntities
from .entity_reader import EntityReader, FilteredEntities
from .oasis_profile_generator import OasisProfileGenerator, OasisAgentProfile
from .simulation_config_generator import SimulationConfigGenerator, SimulationParameters
from ..utils.locale import t
logger = get_logger('mirofish.simulation')
logger = get_logger("mirofish.simulation")
class SimulationStatus(str, Enum):
"""模拟状态"""
"""Simulation status"""
CREATED = "created"
PREPARING = "preparing"
READY = "ready"
RUNNING = "running"
STOPPING = "stopping"
PAUSED = "paused"
STOPPED = "stopped" # 模拟被手动停止
COMPLETED = "completed" # 模拟自然完成
STOPPED = "stopped" # Simulation manually stopped
COMPLETED = "completed" # Simulation naturally completed
FAILED = "failed"
class PlatformType(str, Enum):
"""平台类型"""
"""Platform type"""
TWITTER = "twitter"
REDDIT = "reddit"
@dataclass
class SimulationState:
"""模拟状态"""
"""Simulation state"""
simulation_id: str
project_id: str
graph_id: str
# 平台启用状态
# Platform enable status
enable_twitter: bool = True
enable_reddit: bool = True
# 状态
# Status
status: SimulationStatus = SimulationStatus.CREATED
# 准备阶段数据
# Preparation phase data
entities_count: int = 0
profiles_count: int = 0
entity_types: List[str] = field(default_factory=list)
# 配置生成信息
profiles_generated: bool = False
# Config generation info
config_generated: bool = False
config_reasoning: str = ""
# 运行时数据
# Runtime data
current_round: int = 0
twitter_status: str = "not_started"
reddit_status: str = "not_started"
# 时间戳
# Timestamps
created_at: str = field(default_factory=lambda: datetime.now().isoformat())
updated_at: str = field(default_factory=lambda: datetime.now().isoformat())
# 错误信息
# Error info
error: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
"""完整状态字典(内部使用)"""
"""Full state dictionary (internal use)"""
return {
"simulation_id": self.simulation_id,
"project_id": self.project_id,
@ -89,7 +90,6 @@ class SimulationState:
"entities_count": self.entities_count,
"profiles_count": self.profiles_count,
"entity_types": self.entity_types,
"profiles_generated": self.profiles_generated,
"config_generated": self.config_generated,
"config_reasoning": self.config_reasoning,
"current_round": self.current_round,
@ -99,18 +99,9 @@ class SimulationState:
"updated_at": self.updated_at,
"error": self.error,
}
def get_default_platform(self) -> str:
"""根据启用状态返回默认平台"""
if self.enable_twitter and self.enable_reddit:
return "reddit" # 两者都启用时保持原默认
elif self.enable_twitter:
return "twitter"
else:
return "reddit"
def to_simple_dict(self) -> Dict[str, Any]:
"""简化状态字典API返回使用"""
"""Simplified state dictionary (for API responses)"""
return {
"simulation_id": self.simulation_id,
"project_id": self.project_id,
@ -119,7 +110,6 @@ class SimulationState:
"entities_count": self.entities_count,
"profiles_count": self.profiles_count,
"entity_types": self.entity_types,
"profiles_generated": self.profiles_generated,
"config_generated": self.config_generated,
"error": self.error,
}
@ -127,60 +117,59 @@ class SimulationState:
class SimulationManager:
"""
模拟管理器
核心功能
1. 从Zep图谱读取实体并过滤
2. 生成OASIS Agent Profile
3. 使用LLM智能生成模拟配置参数
4. 准备预设脚本所需的所有文件
Simulation manager
Core features:
1. Read entities from the graph store and filter them
2. Generate OASIS Agent Profiles
3. Use LLM to intelligently generate simulation configuration parameters
4. Prepare all files required by the preset scripts
"""
# 模拟数据存储目录
# Simulation data storage directory
SIMULATION_DATA_DIR = os.path.join(
os.path.dirname(__file__),
'../../uploads/simulations'
os.path.dirname(__file__), "../../uploads/simulations"
)
def __init__(self):
# 确保目录存在
# Ensure directory exists
os.makedirs(self.SIMULATION_DATA_DIR, exist_ok=True)
# 内存中的模拟状态缓存
# In-memory simulation state cache
self._simulations: Dict[str, SimulationState] = {}
def _get_simulation_dir(self, simulation_id: str) -> str:
"""获取模拟数据目录"""
"""Get the simulation data directory"""
sim_dir = os.path.join(self.SIMULATION_DATA_DIR, simulation_id)
os.makedirs(sim_dir, exist_ok=True)
return sim_dir
def _save_simulation_state(self, state: SimulationState):
"""保存模拟状态到文件"""
"""Save the simulation state to a file"""
sim_dir = self._get_simulation_dir(state.simulation_id)
state_file = os.path.join(sim_dir, "state.json")
state.updated_at = datetime.now().isoformat()
with open(state_file, 'w', encoding='utf-8') as f:
with open(state_file, "w", encoding="utf-8") as f:
json.dump(state.to_dict(), f, ensure_ascii=False, indent=2)
self._simulations[state.simulation_id] = state
def _load_simulation_state(self, simulation_id: str) -> Optional[SimulationState]:
"""从文件加载模拟状态"""
"""Load the simulation state from a file"""
if simulation_id in self._simulations:
return self._simulations[simulation_id]
sim_dir = self._get_simulation_dir(simulation_id)
state_file = os.path.join(sim_dir, "state.json")
if not os.path.exists(state_file):
return None
with open(state_file, 'r', encoding='utf-8') as f:
with open(state_file, "r", encoding="utf-8") as f:
data = json.load(f)
state = SimulationState(
simulation_id=simulation_id,
project_id=data.get("project_id", ""),
@ -191,7 +180,6 @@ class SimulationManager:
entities_count=data.get("entities_count", 0),
profiles_count=data.get("profiles_count", 0),
entity_types=data.get("entity_types", []),
profiles_generated=data.get("profiles_generated", False),
config_generated=data.get("config_generated", False),
config_reasoning=data.get("config_reasoning", ""),
current_round=data.get("current_round", 0),
@ -201,10 +189,10 @@ class SimulationManager:
updated_at=data.get("updated_at", datetime.now().isoformat()),
error=data.get("error"),
)
self._simulations[simulation_id] = state
return state
def create_simulation(
self,
project_id: str,
@ -213,20 +201,21 @@ class SimulationManager:
enable_reddit: bool = True,
) -> SimulationState:
"""
创建新的模拟
Create a new simulation
Args:
project_id: 项目ID
graph_id: Zep图谱ID
enable_twitter: 是否启用Twitter模拟
enable_reddit: 是否启用Reddit模拟
project_id: Project ID
graph_id: graph store ID
enable_twitter: Whether to enable Twitter simulation
enable_reddit: Whether to enable Reddit simulation
Returns:
SimulationState
"""
import uuid
simulation_id = f"sim_{uuid.uuid4().hex[:12]}"
state = SimulationState(
simulation_id=simulation_id,
project_id=project_id,
@ -235,12 +224,14 @@ class SimulationManager:
enable_reddit=enable_reddit,
status=SimulationStatus.CREATED,
)
self._save_simulation_state(state)
logger.info(f"创建模拟: {simulation_id}, project={project_id}, graph={graph_id}")
logger.info(
f"Created simulation: {simulation_id}, project={project_id}, graph={graph_id}"
)
return state
def prepare_simulation(
self,
simulation_id: str,
@ -249,102 +240,100 @@ class SimulationManager:
defined_entity_types: Optional[List[str]] = None,
use_llm_for_profiles: bool = True,
progress_callback: Optional[callable] = None,
parallel_profile_count: int = 3
parallel_profile_count: int = 3,
) -> SimulationState:
"""
准备模拟环境全程自动化
步骤
1. 从Zep图谱读取并过滤实体
2. 为每个实体生成OASIS Agent Profile可选LLM增强支持并行
3. 使用LLM智能生成模拟配置参数时间活跃度发言频率等
4. 保存配置文件和Profile文件
5. 复制预设脚本到模拟目录
Prepare the simulation environment (fully automated)
Steps:
1. Read and filter entities from the graph store
2. Generate an OASIS Agent Profile for each entity (optional LLM enhancement, supports parallelism)
3. Use LLM to intelligently generate simulation configuration parameters (time, activity, posting frequency, etc.)
4. Save the configuration file and Profile files
5. Copy preset scripts to the simulation directory
Args:
simulation_id: 模拟ID
simulation_requirement: 模拟需求描述用于LLM生成配置
document_text: 原始文档内容用于LLM理解背景
defined_entity_types: 预定义的实体类型可选
use_llm_for_profiles: 是否使用LLM生成详细人设
progress_callback: 进度回调函数 (stage, progress, message)
parallel_profile_count: 并行生成人设的数量默认3
simulation_id: Simulation ID
simulation_requirement: Simulation requirement description (used for LLM configuration generation)
document_text: Original document content (used for LLM to understand context)
defined_entity_types: Predefined entity types (optional)
use_llm_for_profiles: Whether to use LLM to generate detailed personas
progress_callback: Progress callback function (stage, progress, message)
parallel_profile_count: Number of personas to generate in parallel, default 3
Returns:
SimulationState
"""
state = self._load_simulation_state(simulation_id)
if not state:
raise ValueError(f"模拟不存在: {simulation_id}")
raise ValueError(t("api.simulationNotFound", id=simulation_id))
try:
state.status = SimulationStatus.PREPARING
state.error = None
state.profiles_generated = False
state.config_generated = False
state.config_reasoning = ""
self._save_simulation_state(state)
sim_dir = self._get_simulation_dir(simulation_id)
# ========== 阶段1: 读取并过滤实体 ==========
# ========== Phase 1: Read and filter entities ==========
if progress_callback:
progress_callback("reading", 0, t('progress.connectingZepGraph'))
reader = ZepEntityReader()
progress_callback("reading", 0, t("progress.connectingGraph"))
reader = EntityReader()
if progress_callback:
progress_callback("reading", 30, t('progress.readingNodeData'))
progress_callback("reading", 30, t("progress.readingNodeData"))
filtered = reader.filter_defined_entities(
graph_id=state.graph_id,
defined_entity_types=defined_entity_types,
enrich_with_edges=True
enrich_with_edges=True,
)
state.entities_count = filtered.filtered_count
state.entity_types = list(filtered.entity_types)
if progress_callback:
progress_callback(
"reading", 100,
t('progress.readingComplete', count=filtered.filtered_count),
"reading",
100,
t("progress.readingComplete", count=filtered.filtered_count),
current=filtered.filtered_count,
total=filtered.filtered_count
total=filtered.filtered_count,
)
if filtered.filtered_count == 0:
state.status = SimulationStatus.FAILED
state.error = "没有找到符合条件的实体,请检查图谱是否正确构建"
state.error = "No matching entities found; please check whether the graph is correctly built"
self._save_simulation_state(state)
raise ValueError(state.error)
# ========== 阶段2: 生成Agent Profile ==========
return state
# ========== Phase 2: Generate Agent Profiles ==========
total_entities = len(filtered.entities)
if progress_callback:
progress_callback(
"generating_profiles", 0,
t('progress.startGenerating'),
"generating_profiles",
0,
t("progress.startGenerating"),
current=0,
total=total_entities
total=total_entities,
)
# 传入graph_id以启用Zep检索功能获取更丰富的上下文
# Pass graph_id to enable graph retrieval for richer context
generator = OasisProfileGenerator(graph_id=state.graph_id)
def profile_progress(current, total, msg):
if progress_callback:
progress_callback(
"generating_profiles",
int(current / total * 100),
"generating_profiles",
int(current / total * 100),
msg,
current=current,
total=total,
item_name=msg
item_name=msg,
)
# 设置实时保存的文件路径(优先使用 Reddit JSON 格式)
# Set real-time save file path (prefer Reddit JSON format)
realtime_output_path = None
realtime_platform = "reddit"
if state.enable_reddit:
@ -353,73 +342,75 @@ class SimulationManager:
elif state.enable_twitter:
realtime_output_path = os.path.join(sim_dir, "twitter_profiles.csv")
realtime_platform = "twitter"
profiles = generator.generate_profiles_from_entities(
entities=filtered.entities,
use_llm=use_llm_for_profiles,
progress_callback=profile_progress,
graph_id=state.graph_id, # 传入graph_id用于Zep检索
parallel_count=parallel_profile_count, # 并行生成数量
realtime_output_path=realtime_output_path, # 实时保存路径
output_platform=realtime_platform # 输出格式
graph_id=state.graph_id, # Pass graph_id for graph retrieval
parallel_count=parallel_profile_count, # Parallel generation count
realtime_output_path=realtime_output_path, # Real-time save path
output_platform=realtime_platform, # Output format
)
state.profiles_count = len(profiles)
state.profiles_generated = len(profiles) > 0
self._save_simulation_state(state)
# 保存Profile文件注意Twitter使用CSV格式Reddit使用JSON格式
# Reddit 已经在生成过程中实时保存了,这里再保存一次确保完整性
# Save Profile files (note: Twitter uses CSV format, Reddit uses JSON format)
# Reddit was already saved in real time during generation; save again here to ensure completeness
if progress_callback:
progress_callback(
"generating_profiles", 95,
t('progress.savingProfiles'),
"generating_profiles",
95,
t("progress.savingProfiles"),
current=total_entities,
total=total_entities
total=total_entities,
)
if state.enable_reddit:
generator.save_profiles(
profiles=profiles,
file_path=os.path.join(sim_dir, "reddit_profiles.json"),
platform="reddit"
platform="reddit",
)
if state.enable_twitter:
# Twitter使用CSV格式这是OASIS的要求
# Twitter uses CSV format! This is required by OASIS
generator.save_profiles(
profiles=profiles,
file_path=os.path.join(sim_dir, "twitter_profiles.csv"),
platform="twitter"
platform="twitter",
)
if progress_callback:
progress_callback(
"generating_profiles", 100,
t('progress.profilesComplete', count=len(profiles)),
"generating_profiles",
100,
t("progress.profilesComplete", count=len(profiles)),
current=len(profiles),
total=len(profiles)
total=len(profiles),
)
# ========== 阶段3: LLM智能生成模拟配置 ==========
# ========== Phase 3: LLM intelligent simulation config generation ==========
if progress_callback:
progress_callback(
"generating_config", 0,
t('progress.analyzingRequirements'),
"generating_config",
0,
t("progress.analyzingRequirements"),
current=0,
total=3
total=3,
)
config_generator = SimulationConfigGenerator()
if progress_callback:
progress_callback(
"generating_config", 30,
t('progress.callingLLMConfig'),
"generating_config",
30,
t("progress.callingLLMConfig"),
current=1,
total=3
total=3,
)
sim_params = config_generator.generate_config(
simulation_id=simulation_id,
project_id=state.project_id,
@ -428,123 +419,119 @@ class SimulationManager:
document_text=document_text,
entities=filtered.entities,
enable_twitter=state.enable_twitter,
enable_reddit=state.enable_reddit
enable_reddit=state.enable_reddit,
)
if progress_callback:
progress_callback(
"generating_config", 70,
t('progress.savingConfigFiles'),
"generating_config",
70,
t("progress.savingConfigFiles"),
current=2,
total=3
total=3,
)
# 保存配置文件
# Save config files
config_path = os.path.join(sim_dir, "simulation_config.json")
with open(config_path, 'w', encoding='utf-8') as f:
with open(config_path, "w", encoding="utf-8") as f:
f.write(sim_params.to_json())
state.config_generated = True
state.config_reasoning = sim_params.generation_reasoning
if progress_callback:
progress_callback(
"generating_config", 100,
t('progress.configComplete'),
"generating_config",
100,
t("progress.configComplete"),
current=3,
total=3
total=3,
)
# 注意:运行脚本保留在 backend/scripts/ 目录,不再复制到模拟目录
# 启动模拟时simulation_runner 会从 scripts/ 目录运行脚本
# 更新状态
# Note: run scripts stay in backend/scripts/, no longer copied to simulation dir
# When starting simulation, simulation_runner runs scripts from scripts/ dir
# Update state
state.status = SimulationStatus.READY
self._save_simulation_state(state)
logger.info(f"模拟准备完成: {simulation_id}, "
f"entities={state.entities_count}, profiles={state.profiles_count}")
logger.info(
f"Simulation preparation complete: {simulation_id}, "
f"entities={state.entities_count}, profiles={state.profiles_count}"
)
return state
except Exception as e:
logger.error(f"模拟准备失败: {simulation_id}, error={str(e)}")
logger.error(f"Simulation preparation failed: {simulation_id}, error={str(e)}")
import traceback
logger.error(traceback.format_exc())
state.status = SimulationStatus.FAILED
state.error = str(e)
self._save_simulation_state(state)
raise
def get_simulation(self, simulation_id: str) -> Optional[SimulationState]:
"""获取模拟状态"""
"""Get simulation state"""
return self._load_simulation_state(simulation_id)
def list_simulations(self, project_id: Optional[str] = None) -> List[SimulationState]:
"""列出所有模拟"""
def list_simulations(
self, project_id: Optional[str] = None
) -> List[SimulationState]:
"""List all simulations"""
simulations = []
if os.path.exists(self.SIMULATION_DATA_DIR):
for sim_id in os.listdir(self.SIMULATION_DATA_DIR):
# 跳过隐藏文件(如 .DS_Store和非目录文件
# Skip hidden files (e.g. .DS_Store) and non-directory files
sim_path = os.path.join(self.SIMULATION_DATA_DIR, sim_id)
if sim_id.startswith('.') or not os.path.isdir(sim_path):
if sim_id.startswith(".") or not os.path.isdir(sim_path):
continue
state = self._load_simulation_state(sim_id)
if state:
if project_id is None or state.project_id == project_id:
simulations.append(state)
return simulations
def get_profiles(self, simulation_id: str, platform: str = None) -> List[Dict[str, Any]]:
"""获取模拟的Agent Profile"""
def get_profiles(
self, simulation_id: str, platform: str = "reddit"
) -> List[Dict[str, Any]]:
"""Get the simulation's Agent Profile"""
state = self._load_simulation_state(simulation_id)
if not state:
raise ValueError(f"模拟不存在: {simulation_id}")
if platform is None:
platform = state.get_default_platform()
if platform not in {"twitter", "reddit"}:
raise ValueError(f"不支持的平台: {platform}")
raise ValueError(t("api.simulationNotFound", id=simulation_id))
sim_dir = self._get_simulation_dir(simulation_id)
profile_path = os.path.join(
sim_dir,
"twitter_profiles.csv" if platform == "twitter" else "reddit_profiles.json",
)
profile_path = os.path.join(sim_dir, f"{platform}_profiles.json")
if not os.path.exists(profile_path):
return []
if platform == "twitter":
import csv
with open(profile_path, 'r', encoding='utf-8', newline='') as f:
return list(csv.DictReader(f))
with open(profile_path, 'r', encoding='utf-8') as f:
with open(profile_path, "r", encoding="utf-8") as f:
return json.load(f)
def get_simulation_config(self, simulation_id: str) -> Optional[Dict[str, Any]]:
"""获取模拟配置"""
"""Get the simulation configuration"""
sim_dir = self._get_simulation_dir(simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json")
if not os.path.exists(config_path):
return None
with open(config_path, 'r', encoding='utf-8') as f:
with open(config_path, "r", encoding="utf-8") as f:
return json.load(f)
def get_run_instructions(self, simulation_id: str) -> Dict[str, str]:
"""获取运行说明"""
"""Get run instructions"""
sim_dir = self._get_simulation_dir(simulation_id)
config_path = os.path.join(sim_dir, "simulation_config.json")
scripts_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../scripts'))
scripts_dir = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../scripts")
)
return {
"simulation_dir": sim_dir,
"scripts_dir": scripts_dir,
@ -555,10 +542,10 @@ class SimulationManager:
"parallel": f"python {scripts_dir}/run_parallel_simulation.py --config {config_path}",
},
"instructions": (
f"1. 激活conda环境: conda activate MiroFish\n"
f"2. 运行模拟 (脚本位于 {scripts_dir}):\n"
f" - 单独运行Twitter: python {scripts_dir}/run_twitter_simulation.py --config {config_path}\n"
f" - 单独运行Reddit: python {scripts_dir}/run_reddit_simulation.py --config {config_path}\n"
f" - 并行运行双平台: python {scripts_dir}/run_parallel_simulation.py --config {config_path}"
)
f"1. Activate conda environment: conda activate MiroFish\n"
f"2. Run simulation (scripts located at {scripts_dir}):\n"
f" - Run Twitter only: python {scripts_dir}/run_twitter_simulation.py --config {config_path}\n"
f" - Run Reddit only: python {scripts_dir}/run_reddit_simulation.py --config {config_path}\n"
f" - Run both platforms in parallel: python {scripts_dir}/run_parallel_simulation.py --config {config_path}"
),
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
"""
文本处理服务
Text processing service
"""
from typing import List, Optional
@ -7,65 +7,60 @@ from ..utils.file_parser import FileParser, split_text_into_chunks
class TextProcessor:
"""文本处理器"""
"""Text processor"""
@staticmethod
def extract_from_files(file_paths: List[str]) -> str:
"""从多个文件提取文本"""
"""Extract text from multiple files"""
return FileParser.extract_from_multiple(file_paths)
@staticmethod
def split_text(
text: str,
chunk_size: int = 500,
overlap: int = 50
) -> List[str]:
def split_text(text: str, chunk_size: int = 500, overlap: int = 50) -> List[str]:
"""
分割文本
Split text
Args:
text: 原始文本
chunk_size: 块大小
overlap: 重叠大小
text: Original text
chunk_size: Chunk size
overlap: Overlap size
Returns:
文本块列表
List of text chunks
"""
return split_text_into_chunks(text, chunk_size, overlap)
@staticmethod
def preprocess_text(text: str) -> str:
"""
预处理文本
- 移除多余空白
- 标准化换行
Preprocess text
- Remove extra whitespace
- Normalize line breaks
Args:
text: 原始文本
text: Original text
Returns:
处理后的文本
Processed text
"""
import re
# 标准化换行
text = text.replace('\r\n', '\n').replace('\r', '\n')
# 移除连续空行(保留最多两个换行)
text = re.sub(r'\n{3,}', '\n\n', text)
# 移除行首行尾空白
lines = [line.strip() for line in text.split('\n')]
text = '\n'.join(lines)
# Normalize line breaks
text = text.replace("\r\n", "\n").replace("\r", "\n")
# Remove consecutive blank lines (keep at most two newlines)
text = re.sub(r"\n{3,}", "\n\n", text)
# Strip leading and trailing whitespace from each line
lines = [line.strip() for line in text.split("\n")]
text = "\n".join(lines)
return text.strip()
@staticmethod
def get_text_stats(text: str) -> dict:
"""获取文本统计信息"""
"""Get text statistics"""
return {
"total_chars": len(text),
"total_lines": text.count('\n') + 1,
"total_lines": text.count("\n") + 1,
"total_words": len(text.split()),
}

View File

@ -1,446 +0,0 @@
"""
Zep实体读取与过滤服务
从Zep图谱中读取节点筛选出符合预定义实体类型的节点
"""
from typing import Dict, Any, List, Optional, Set, Callable, TypeVar
from dataclasses import dataclass, field
from zep_cloud import NotFoundError
from ..config import Config
from ..utils.logger import get_logger
from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges
from ..utils.zep import call_zep_read_with_retry, get_zep_client
logger = get_logger('mirofish.zep_entity_reader')
# 用于泛型返回类型
T = TypeVar('T')
@dataclass
class EntityNode:
"""实体节点数据结构"""
uuid: str
name: str
labels: List[str]
summary: str
attributes: Dict[str, Any]
# 相关的边信息
related_edges: List[Dict[str, Any]] = field(default_factory=list)
# 相关的其他节点信息
related_nodes: List[Dict[str, Any]] = field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"uuid": self.uuid,
"name": self.name,
"labels": self.labels,
"summary": self.summary,
"attributes": self.attributes,
"related_edges": self.related_edges,
"related_nodes": self.related_nodes,
}
def get_entity_type(self) -> Optional[str]:
"""获取实体类型排除默认的Entity标签"""
for label in self.labels:
if label not in ["Entity", "Node"]:
return label
return None
@dataclass
class FilteredEntities:
"""过滤后的实体集合"""
entities: List[EntityNode]
entity_types: Set[str]
total_count: int
filtered_count: int
def to_dict(self) -> Dict[str, Any]:
return {
"entities": [e.to_dict() for e in self.entities],
"entity_types": list(self.entity_types),
"total_count": self.total_count,
"filtered_count": self.filtered_count,
}
class ZepEntityReader:
"""
Zep实体读取与过滤服务
主要功能
1. 从Zep图谱读取所有节点
2. 筛选出符合预定义实体类型的节点Labels不只是Entity的节点
3. 获取每个实体的相关边和关联节点信息
"""
def __init__(self, api_key: Optional[str] = None):
self.api_key = api_key or Config.ZEP_API_KEY
if not self.api_key:
raise ValueError("ZEP_API_KEY 未配置")
self.client = get_zep_client(self.api_key)
def _call_with_retry(
self,
func: Callable[[], T],
operation_name: str,
max_retries: int = 3,
initial_delay: float = 2.0
) -> T:
"""
带重试机制的Zep API调用
Args:
func: 要执行的函数无参数的lambda或callable
operation_name: 操作名称用于日志
max_retries: 最大重试次数默认3次即最多尝试3次
initial_delay: 初始延迟秒数
Returns:
API调用结果
"""
return call_zep_read_with_retry(
func,
operation_name=operation_name,
max_attempts=max_retries,
initial_delay=initial_delay,
)
def get_all_nodes(self, graph_id: str) -> List[Dict[str, Any]]:
"""
获取图谱的所有节点分页获取
Args:
graph_id: 图谱ID
Returns:
节点列表
"""
logger.info(f"获取图谱 {graph_id} 的所有节点...")
nodes = fetch_all_nodes(self.client, graph_id)
nodes_data = []
for node in nodes:
nodes_data.append({
"uuid": getattr(node, 'uuid_', None) or getattr(node, 'uuid', ''),
"name": node.name or "",
"labels": node.labels or [],
"summary": node.summary or "",
"attributes": node.attributes or {},
})
logger.info(f"共获取 {len(nodes_data)} 个节点")
return nodes_data
def get_all_edges(self, graph_id: str) -> List[Dict[str, Any]]:
"""
获取图谱的所有边分页获取
Args:
graph_id: 图谱ID
Returns:
边列表
"""
logger.info(f"获取图谱 {graph_id} 的所有边...")
edges = fetch_all_edges(self.client, graph_id)
edges_data = []
for edge in edges:
edges_data.append({
"uuid": getattr(edge, 'uuid_', None) or getattr(edge, 'uuid', ''),
"name": edge.name or "",
"fact": edge.fact or "",
"source_node_uuid": edge.source_node_uuid,
"target_node_uuid": edge.target_node_uuid,
"attributes": edge.attributes or {},
})
logger.info(f"共获取 {len(edges_data)} 条边")
return edges_data
def get_node_edges(
self,
node_uuid: str,
*,
graph_id: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""
获取指定节点的相关边
Zep Cloud 3.25 ``graph.node.get_edges`` 实测只返回节点作为
source 的边尽管文档将其描述为all edges需要完整上下文时必须
提供 graph_id以全图分页后同时筛选 incoming outgoing
Args:
node_uuid: 节点UUID
graph_id: 图谱ID提供时保证返回双向完整关系
Returns:
边列表
"""
try:
if graph_id:
return [
edge
for edge in self.get_all_edges(graph_id)
if edge["source_node_uuid"] == node_uuid
or edge["target_node_uuid"] == node_uuid
]
# 使用重试机制调用Zep API
edges = self._call_with_retry(
func=lambda: self.client.graph.node.get_edges(node_uuid=node_uuid),
operation_name=f"获取节点边(node={node_uuid[:8]}...)"
)
edges_data = []
for edge in edges:
edges_data.append({
"uuid": getattr(edge, 'uuid_', None) or getattr(edge, 'uuid', ''),
"name": edge.name or "",
"fact": edge.fact or "",
"source_node_uuid": edge.source_node_uuid,
"target_node_uuid": edge.target_node_uuid,
"attributes": edge.attributes or {},
})
return edges_data
except Exception as e:
# An empty edge list is valid data. Authentication, permission and
# transport failures must not be made indistinguishable from it.
logger.error(f"获取节点 {node_uuid} 的边失败: {str(e)}")
raise
def filter_defined_entities(
self,
graph_id: str,
defined_entity_types: Optional[List[str]] = None,
enrich_with_edges: bool = True
) -> FilteredEntities:
"""
筛选出符合预定义实体类型的节点
筛选逻辑
- 如果节点的Labels只有一个"Entity"说明这个实体不符合我们预定义的类型跳过
- 如果节点的Labels包含除"Entity""Node"之外的标签说明符合预定义类型保留
Args:
graph_id: 图谱ID
defined_entity_types: 预定义的实体类型列表可选如果提供则只保留这些类型
enrich_with_edges: 是否获取每个实体的相关边信息
Returns:
FilteredEntities: 过滤后的实体集合
"""
logger.info(f"开始筛选图谱 {graph_id} 的实体...")
# 获取所有节点
all_nodes = self.get_all_nodes(graph_id)
total_count = len(all_nodes)
# 获取所有边(用于后续关联查找)
all_edges = self.get_all_edges(graph_id) if enrich_with_edges else []
# 构建节点UUID到节点数据的映射
node_map = {n["uuid"]: n for n in all_nodes}
# 筛选符合条件的实体
filtered_entities = []
entity_types_found = set()
for node in all_nodes:
labels = node.get("labels", [])
# 筛选逻辑Labels必须包含除"Entity"和"Node"之外的标签
custom_labels = [l for l in labels if l not in ["Entity", "Node"]]
if not custom_labels:
# 只有默认标签,跳过
continue
# 如果指定了预定义类型,检查是否匹配
if defined_entity_types:
matching_labels = [l for l in custom_labels if l in defined_entity_types]
if not matching_labels:
continue
entity_type = matching_labels[0]
else:
entity_type = custom_labels[0]
entity_types_found.add(entity_type)
# 创建实体节点对象
entity = EntityNode(
uuid=node["uuid"],
name=node["name"],
labels=labels,
summary=node["summary"],
attributes=node["attributes"],
)
# 获取相关边和节点
if enrich_with_edges:
related_edges = []
related_node_uuids = set()
for edge in all_edges:
if edge["source_node_uuid"] == node["uuid"]:
related_edges.append({
"direction": "outgoing",
"edge_name": edge["name"],
"fact": edge["fact"],
"target_node_uuid": edge["target_node_uuid"],
})
related_node_uuids.add(edge["target_node_uuid"])
elif edge["target_node_uuid"] == node["uuid"]:
related_edges.append({
"direction": "incoming",
"edge_name": edge["name"],
"fact": edge["fact"],
"source_node_uuid": edge["source_node_uuid"],
})
related_node_uuids.add(edge["source_node_uuid"])
entity.related_edges = related_edges
# 获取关联节点的基本信息
related_nodes = []
for related_uuid in related_node_uuids:
if related_uuid in node_map:
related_node = node_map[related_uuid]
related_nodes.append({
"uuid": related_node["uuid"],
"name": related_node["name"],
"labels": related_node["labels"],
"summary": related_node.get("summary", ""),
})
entity.related_nodes = related_nodes
filtered_entities.append(entity)
logger.info(f"筛选完成: 总节点 {total_count}, 符合条件 {len(filtered_entities)}, "
f"实体类型: {entity_types_found}")
return FilteredEntities(
entities=filtered_entities,
entity_types=entity_types_found,
total_count=total_count,
filtered_count=len(filtered_entities),
)
def get_entity_with_context(
self,
graph_id: str,
entity_uuid: str
) -> Optional[EntityNode]:
"""
获取单个实体及其完整上下文边和关联节点带重试机制
Args:
graph_id: 图谱ID
entity_uuid: 实体UUID
Returns:
EntityNode或None
"""
try:
# 使用重试机制获取节点
node = self._call_with_retry(
func=lambda: self.client.graph.node.get(uuid_=entity_uuid),
operation_name=f"获取节点详情(uuid={entity_uuid[:8]}...)"
)
if not node:
return None
# 获取节点的边
edges = self.get_node_edges(entity_uuid, graph_id=graph_id)
# 获取所有节点用于关联查找
all_nodes = self.get_all_nodes(graph_id)
node_map = {n["uuid"]: n for n in all_nodes}
# 处理相关边和节点
related_edges = []
related_node_uuids = set()
for edge in edges:
if edge["source_node_uuid"] == entity_uuid:
related_edges.append({
"direction": "outgoing",
"edge_name": edge["name"],
"fact": edge["fact"],
"target_node_uuid": edge["target_node_uuid"],
})
related_node_uuids.add(edge["target_node_uuid"])
else:
related_edges.append({
"direction": "incoming",
"edge_name": edge["name"],
"fact": edge["fact"],
"source_node_uuid": edge["source_node_uuid"],
})
related_node_uuids.add(edge["source_node_uuid"])
# 获取关联节点信息
related_nodes = []
for related_uuid in related_node_uuids:
if related_uuid in node_map:
related_node = node_map[related_uuid]
related_nodes.append({
"uuid": related_node["uuid"],
"name": related_node["name"],
"labels": related_node["labels"],
"summary": related_node.get("summary", ""),
})
return EntityNode(
uuid=getattr(node, 'uuid_', None) or getattr(node, 'uuid', ''),
name=node.name or "",
labels=node.labels or [],
summary=node.summary or "",
attributes=node.attributes or {},
related_edges=related_edges,
related_nodes=related_nodes,
)
except NotFoundError:
return None
except Exception as e:
# Only an actual Zep 404 means "entity not found". Propagate 401,
# 403 and exhausted transport errors so callers cannot prepare a
# simulation with silently incomplete graph context.
logger.error(f"获取实体 {entity_uuid} 失败: {str(e)}")
raise
def get_entities_by_type(
self,
graph_id: str,
entity_type: str,
enrich_with_edges: bool = True
) -> List[EntityNode]:
"""
获取指定类型的所有实体
Args:
graph_id: 图谱ID
entity_type: 实体类型 "Student", "PublicFigure"
enrich_with_edges: 是否获取相关边信息
Returns:
实体列表
"""
result = self.filter_defined_entities(
graph_id=graph_id,
defined_entity_types=[entity_type],
enrich_with_edges=enrich_with_edges
)
return result.entities

View File

@ -1,791 +0,0 @@
"""
Zep图谱记忆更新服务
将模拟中的Agent活动动态更新到Zep图谱中
"""
import time
import threading
from typing import Dict, Any, List, Optional
from dataclasses import dataclass
from datetime import datetime
from queue import Queue, Empty
from ..config import Config
from ..utils.logger import get_logger
from ..utils.locale import get_locale, set_locale
from ..utils.zep import (
ZEP_INGESTION_WAIT_TIMEOUT_SECONDS,
call_zep_read_with_retry,
get_zep_client,
)
logger = get_logger('mirofish.zep_graph_memory_updater')
@dataclass
class AgentActivity:
"""Agent活动记录"""
platform: str # twitter / reddit
agent_id: int
agent_name: str
action_type: str # CREATE_POST, LIKE_POST, etc.
action_args: Dict[str, Any]
round_num: int
timestamp: str
def to_episode_text(self) -> str:
"""
将活动转换为可以发送给Zep的文本描述
采用自然语言描述格式让Zep能够从中提取实体和关系
不添加模拟相关的前缀避免误导图谱更新
"""
# 根据不同的动作类型生成不同的描述
action_descriptions = {
"CREATE_POST": self._describe_create_post,
"LIKE_POST": self._describe_like_post,
"DISLIKE_POST": self._describe_dislike_post,
"REPOST": self._describe_repost,
"QUOTE_POST": self._describe_quote_post,
"FOLLOW": self._describe_follow,
"CREATE_COMMENT": self._describe_create_comment,
"LIKE_COMMENT": self._describe_like_comment,
"DISLIKE_COMMENT": self._describe_dislike_comment,
"SEARCH_POSTS": self._describe_search,
"SEARCH_USER": self._describe_search_user,
"MUTE": self._describe_mute,
}
describe_func = action_descriptions.get(self.action_type, self._describe_generic)
description = describe_func()
# Keep the event time in the source text as well as episode metadata so
# temporal extraction does not collapse a multi-action batch.
return (
f"[{self.timestamp}] [{self.platform} round {self.round_num}] "
f"{self.agent_name}: {description}"
)
def _describe_create_post(self) -> str:
content = self.action_args.get("content", "")
if content:
return f"发布了一条帖子:「{content}"
return "发布了一条帖子"
def _describe_like_post(self) -> str:
"""点赞帖子 - 包含帖子原文和作者信息"""
post_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "")
if post_content and post_author:
return f"点赞了{post_author}的帖子:「{post_content}"
elif post_content:
return f"点赞了一条帖子:「{post_content}"
elif post_author:
return f"点赞了{post_author}的一条帖子"
return "点赞了一条帖子"
def _describe_dislike_post(self) -> str:
"""踩帖子 - 包含帖子原文和作者信息"""
post_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "")
if post_content and post_author:
return f"踩了{post_author}的帖子:「{post_content}"
elif post_content:
return f"踩了一条帖子:「{post_content}"
elif post_author:
return f"踩了{post_author}的一条帖子"
return "踩了一条帖子"
def _describe_repost(self) -> str:
"""转发帖子 - 包含原帖内容和作者信息"""
original_content = self.action_args.get("original_content", "")
original_author = self.action_args.get("original_author_name", "")
if original_content and original_author:
return f"转发了{original_author}的帖子:「{original_content}"
elif original_content:
return f"转发了一条帖子:「{original_content}"
elif original_author:
return f"转发了{original_author}的一条帖子"
return "转发了一条帖子"
def _describe_quote_post(self) -> str:
"""引用帖子 - 包含原帖内容、作者信息和引用评论"""
original_content = self.action_args.get("original_content", "")
original_author = self.action_args.get("original_author_name", "")
quote_content = self.action_args.get("quote_content", "") or self.action_args.get("content", "")
base = ""
if original_content and original_author:
base = f"引用了{original_author}的帖子「{original_content}"
elif original_content:
base = f"引用了一条帖子「{original_content}"
elif original_author:
base = f"引用了{original_author}的一条帖子"
else:
base = "引用了一条帖子"
if quote_content:
base += f",并评论道:「{quote_content}"
return base
def _describe_follow(self) -> str:
"""关注用户 - 包含被关注用户的名称"""
target_user_name = self.action_args.get("target_user_name", "")
if target_user_name:
return f"关注了用户「{target_user_name}"
return "关注了一个用户"
def _describe_create_comment(self) -> str:
"""发表评论 - 包含评论内容和所评论的帖子信息"""
content = self.action_args.get("content", "")
post_content = self.action_args.get("post_content", "")
post_author = self.action_args.get("post_author_name", "")
if content:
if post_content and post_author:
return f"{post_author}的帖子「{post_content}」下评论道:「{content}"
elif post_content:
return f"在帖子「{post_content}」下评论道:「{content}"
elif post_author:
return f"{post_author}的帖子下评论道:「{content}"
return f"评论道:「{content}"
return "发表了评论"
def _describe_like_comment(self) -> str:
"""点赞评论 - 包含评论内容和作者信息"""
comment_content = self.action_args.get("comment_content", "")
comment_author = self.action_args.get("comment_author_name", "")
if comment_content and comment_author:
return f"点赞了{comment_author}的评论:「{comment_content}"
elif comment_content:
return f"点赞了一条评论:「{comment_content}"
elif comment_author:
return f"点赞了{comment_author}的一条评论"
return "点赞了一条评论"
def _describe_dislike_comment(self) -> str:
"""踩评论 - 包含评论内容和作者信息"""
comment_content = self.action_args.get("comment_content", "")
comment_author = self.action_args.get("comment_author_name", "")
if comment_content and comment_author:
return f"踩了{comment_author}的评论:「{comment_content}"
elif comment_content:
return f"踩了一条评论:「{comment_content}"
elif comment_author:
return f"踩了{comment_author}的一条评论"
return "踩了一条评论"
def _describe_search(self) -> str:
"""搜索帖子 - 包含搜索关键词"""
query = self.action_args.get("query", "") or self.action_args.get("keyword", "")
return f"搜索了「{query}" if query else "进行了搜索"
def _describe_search_user(self) -> str:
"""搜索用户 - 包含搜索关键词"""
query = self.action_args.get("query", "") or self.action_args.get("username", "")
return f"搜索了用户「{query}" if query else "搜索了用户"
def _describe_mute(self) -> str:
"""屏蔽用户 - 包含被屏蔽用户的名称"""
target_user_name = self.action_args.get("target_user_name", "")
if target_user_name:
return f"屏蔽了用户「{target_user_name}"
return "屏蔽了一个用户"
def _describe_generic(self) -> str:
# 对于未知的动作类型,生成通用描述
return f"执行了{self.action_type}操作"
class _DrainDeadlineExceeded(TimeoutError):
def __init__(self, processed_count: int):
super().__init__("Zep updater drain deadline elapsed")
self.processed_count = processed_count
class ZepGraphMemoryUpdater:
"""
Zep图谱记忆更新器
监控模拟的actions日志文件将新的agent活动实时更新到Zep图谱中
按平台分组每累积BATCH_SIZE条活动后批量发送到Zep
所有有意义的行为都会被更新到Zepaction_args中会包含完整的上下文信息
- 点赞/踩的帖子原文
- 转发/引用的帖子原文
- 关注/屏蔽的用户名
- 点赞/踩的评论原文
"""
# 批量发送大小(每个平台累积多少条后发送)
BATCH_SIZE = 5
# 平台名称映射(用于控制台显示)
PLATFORM_DISPLAY_NAMES = {
'twitter': '世界1',
'reddit': '世界2',
}
# 发送间隔(秒),避免请求过快
SEND_INTERVAL = 0.5
# Zep recommends keeping an episode below 10,000 characters. Leave room
# for future source formatting changes.
MAX_EPISODE_CHARS = 9_500
def __init__(
self,
graph_id: str,
api_key: Optional[str] = None,
simulation_id: Optional[str] = None,
):
"""
初始化更新器
Args:
graph_id: Zep图谱ID
api_key: Zep API Key可选默认从配置读取
"""
self.graph_id = graph_id
self.simulation_id = simulation_id or "unknown"
self.api_key = api_key or Config.ZEP_API_KEY
if not self.api_key:
raise ValueError("ZEP_API_KEY未配置")
self.client = get_zep_client(self.api_key)
# 活动队列
self._activity_queue: Queue = Queue()
# 按平台分组的活动缓冲区每个平台各自累积到BATCH_SIZE后批量发送
self._platform_buffers: Dict[str, List[AgentActivity]] = {
'twitter': [],
'reddit': [],
}
self._buffer_lock = threading.Lock()
self._acceptance_lock = threading.Lock()
# 控制标志
self._running = False
self._worker_thread: Optional[threading.Thread] = None
# 统计
self._total_activities = 0 # 实际添加到队列的活动数
self._total_sent = 0 # 成功发送到Zep的批次数
self._total_items_sent = 0 # 成功发送到Zep的活动条数
self._failed_count = 0 # 发送失败的批次数
self._skipped_count = 0 # 被过滤跳过的活动数DO_NOTHING
self._failed_batches: List[Dict[str, Any]] = []
self._pending_episode_uuids: List[str] = []
logger.info(f"ZepGraphMemoryUpdater 初始化完成: graph_id={graph_id}, batch_size={self.BATCH_SIZE}")
def _get_platform_display_name(self, platform: str) -> str:
"""获取平台的显示名称"""
return self.PLATFORM_DISPLAY_NAMES.get(platform.lower(), platform)
def start(self):
"""启动后台工作线程"""
if self._running:
return
# Capture locale before spawning background thread
current_locale = get_locale()
self._running = True
self._worker_thread = threading.Thread(
target=self._worker_loop,
args=(current_locale,),
daemon=True,
name=f"ZepMemoryUpdater-{self.graph_id[:8]}"
)
self._worker_thread.start()
logger.info(f"ZepGraphMemoryUpdater 已启动: graph_id={self.graph_id}")
def stop(self):
"""Drain the worker, flush tail events, and wait for Cloud ingestion."""
deadline = time.time() + ZEP_INGESTION_WAIT_TIMEOUT_SECONDS
# Serialize the accepting->closed transition with add_activity's
# check+enqueue operation. This closes the small race where a producer
# could enqueue after both the worker and final flush had exited.
with self._acceptance_lock:
self._running = False
if self._worker_thread and self._worker_thread.is_alive():
join_timeout = max(0.0, deadline - time.time())
self._worker_thread.join(timeout=join_timeout)
if self._worker_thread.is_alive():
raise TimeoutError(
f"Zep updater worker did not stop within {join_timeout:.0f}s"
)
# The worker has drained the queue. Only now is it safe to flush
# buffers; doing this before join loses an item already dequeued by the
# worker but not yet buffered.
self._flush_remaining(deadline=deadline)
if self._failed_batches:
raise RuntimeError(
f"{len(self._failed_batches)} Zep activity batch(es) failed; "
"simulation graph ingestion is incomplete"
)
self._wait_for_pending_episodes(deadline=deadline)
logger.info(f"ZepGraphMemoryUpdater 已停止: graph_id={self.graph_id}, "
f"total_activities={self._total_activities}, "
f"batches_sent={self._total_sent}, "
f"items_sent={self._total_items_sent}, "
f"failed={self._failed_count}, "
f"skipped={self._skipped_count}")
def add_activity(self, activity: AgentActivity):
"""
添加一个agent活动到队列
所有有意义的行为都会被添加到队列包括
- CREATE_POST发帖
- CREATE_COMMENT评论
- QUOTE_POST引用帖子
- SEARCH_POSTS搜索帖子
- SEARCH_USER搜索用户
- LIKE_POST/DISLIKE_POST点赞/踩帖子
- REPOST转发
- FOLLOW关注
- MUTE屏蔽
- LIKE_COMMENT/DISLIKE_COMMENT点赞/踩评论
action_args中会包含完整的上下文信息如帖子原文用户名等
Args:
activity: Agent活动记录
"""
# 跳过DO_NOTHING类型的活动
if activity.action_type == "DO_NOTHING":
self._skipped_count += 1
return
with self._acceptance_lock:
if not self._running:
raise RuntimeError("Zep graph updater is not running")
self._activity_queue.put(activity)
self._total_activities += 1
logger.debug(f"添加活动到Zep队列: {activity.agent_name} - {activity.action_type}")
def add_activity_from_dict(self, data: Dict[str, Any], platform: str):
"""
从字典数据添加活动
Args:
data: 从actions.jsonl解析的字典数据
platform: 平台名称 (twitter/reddit)
"""
# 跳过事件类型的条目
if "event_type" in data:
return
if data.get("success") is False:
self._skipped_count += 1
return
activity = AgentActivity(
platform=platform,
agent_id=data.get("agent_id", 0),
agent_name=data.get("agent_name", ""),
action_type=data.get("action_type", ""),
action_args=data.get("action_args", {}),
round_num=data.get("round", 0),
timestamp=data.get("timestamp", datetime.now().isoformat()),
)
self.add_activity(activity)
def _worker_loop(self, locale: str = 'zh'):
"""后台工作循环 - 按平台批量发送活动到Zep"""
set_locale(locale)
while self._running or not self._activity_queue.empty():
try:
# 尝试从队列获取活动超时1秒
try:
activity = self._activity_queue.get(timeout=1)
# 将活动添加到对应平台的缓冲区
platform = activity.platform.lower()
batch = None
with self._buffer_lock:
if platform not in self._platform_buffers:
self._platform_buffers[platform] = []
self._platform_buffers[platform].append(activity)
# 检查该平台是否达到批量大小
if len(self._platform_buffers[platform]) >= self.BATCH_SIZE:
batch = self._platform_buffers[platform][:self.BATCH_SIZE]
self._platform_buffers[platform] = self._platform_buffers[platform][self.BATCH_SIZE:]
# Never hold the buffer lock across network I/O or sleep.
if batch:
self._send_batch_activities(batch, platform)
time.sleep(self.SEND_INTERVAL)
except Empty:
pass
except Exception as e:
logger.error(f"工作循环异常: {e}")
time.sleep(1)
def _build_episode_payloads(
self,
activities: List[AgentActivity],
) -> List[tuple[List[AgentActivity], str]]:
payloads: List[tuple[List[AgentActivity], str]] = []
current_activities: List[AgentActivity] = []
current_lines: List[str] = []
current_length = 0
for activity in activities:
text = activity.to_episode_text()
if len(text) > self.MAX_EPISODE_CHARS:
marker = "... [truncated by MiroFish]"
text = text[: self.MAX_EPISODE_CHARS - len(marker)] + marker
projected_length = current_length + (1 if current_lines else 0) + len(text)
if current_lines and projected_length > self.MAX_EPISODE_CHARS:
payloads.append((current_activities, "\n".join(current_lines)))
current_activities = []
current_lines = []
current_length = 0
current_activities.append(activity)
current_lines.append(text)
current_length += (1 if len(current_lines) > 1 else 0) + len(text)
if current_lines:
payloads.append((current_activities, "\n".join(current_lines)))
return payloads
def _send_batch_activities(
self,
activities: List[AgentActivity],
platform: str,
*,
deadline: float | None = None,
) -> int:
"""
批量发送活动到Zep图谱合并为一条文本
Args:
activities: Agent活动列表
platform: 平台名称
"""
if not activities:
return 0
processed_count = 0
for payload_activities, combined_text in self._build_episode_payloads(activities):
if deadline is not None and time.time() >= deadline:
raise _DrainDeadlineExceeded(processed_count)
try:
episode = self.client.graph.add(
graph_id=self.graph_id,
type="text",
data=combined_text,
created_at=self._to_rfc3339(payload_activities[-1].timestamp),
source_description="MiroFish simulation activity batch",
metadata={
"source": "mirofish_simulation",
"simulation_id": self.simulation_id,
"platform": platform,
"activity_count": len(payload_activities),
"first_round": min(a.round_num for a in payload_activities),
"last_round": max(a.round_num for a in payload_activities),
"agent_ids": ",".join(
str(value)
for value in sorted({a.agent_id for a in payload_activities})
),
"action_types": ",".join(
value
for value in sorted({a.action_type for a in payload_activities})
if value
) or "unknown",
},
)
episode_uuid = (
getattr(episode, "uuid_", None)
or getattr(episode, "uuid", None)
)
if not episode_uuid:
raise RuntimeError("Zep graph.add returned no episode UUID")
self._pending_episode_uuids.append(str(episode_uuid))
self._total_sent += 1
self._total_items_sent += len(payload_activities)
display_name = self._get_platform_display_name(platform)
logger.info(f"成功批量发送 {len(payload_activities)}{display_name}活动到图谱 {self.graph_id}")
logger.debug(f"批量内容预览: {combined_text[:200]}...")
except Exception as e:
# graph.add has no idempotency key. Replaying an ambiguous
# response can duplicate extracted facts, so fail closed and
# surface the incomplete batch to SimulationRunner.
logger.error(f"批量发送到Zep失败未自动重放非幂等写入: {e}")
self._failed_count += 1
self._failed_batches.append({
"platform": platform,
"activities": payload_activities,
"error": str(e),
})
finally:
# Successes have a confirmed episode UUID; failures are kept
# durably in _failed_batches and must never be replayed. Either
# way this payload is accounted for before moving on.
processed_count += len(payload_activities)
return processed_count
@staticmethod
def _to_rfc3339(value: str) -> str:
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
if parsed.tzinfo is None:
parsed = parsed.astimezone()
return parsed.isoformat()
except (AttributeError, TypeError, ValueError):
return datetime.now().astimezone().isoformat()
def _flush_remaining(self, *, deadline: float | None = None):
"""发送队列和缓冲区中剩余的活动"""
# 首先处理队列中剩余的活动,添加到缓冲区
while not self._activity_queue.empty():
try:
activity = self._activity_queue.get_nowait()
platform = activity.platform.lower()
with self._buffer_lock:
if platform not in self._platform_buffers:
self._platform_buffers[platform] = []
self._platform_buffers[platform].append(activity)
except Empty:
break
for platform in list(self._platform_buffers):
with self._buffer_lock:
buffer = list(self._platform_buffers.get(platform, []))
if not buffer:
continue
display_name = self._get_platform_display_name(platform)
logger.info(f"发送{display_name}平台剩余的 {len(buffer)} 条活动")
if deadline is not None and time.time() >= deadline:
raise TimeoutError(
"Zep updater drain deadline elapsed before flushing all activities"
)
try:
processed_count = self._send_batch_activities(
buffer,
platform,
deadline=deadline,
)
except _DrainDeadlineExceeded as error:
with self._buffer_lock:
del self._platform_buffers[platform][:error.processed_count]
raise TimeoutError(str(error)) from error
else:
with self._buffer_lock:
del self._platform_buffers[platform][:processed_count]
def _wait_for_pending_episodes(self, *, deadline: float | None = None) -> None:
pending = set(self._pending_episode_uuids)
if not pending:
return
if deadline is None:
deadline = time.time() + ZEP_INGESTION_WAIT_TIMEOUT_SECONDS
while pending:
if time.time() >= deadline:
raise TimeoutError(
f"Zep simulation ingestion timed out with {len(pending)} "
"episode(s) pending"
)
for episode_uuid in list(pending):
episode = call_zep_read_with_retry(
lambda: self.client.graph.episode.get(uuid_=episode_uuid),
operation_name=f"poll simulation episode {episode_uuid}",
)
if getattr(episode, "processed", False):
pending.remove(episode_uuid)
if pending:
time.sleep(3)
self._pending_episode_uuids = []
def get_stats(self) -> Dict[str, Any]:
"""获取统计信息"""
with self._buffer_lock:
buffer_sizes = {p: len(b) for p, b in self._platform_buffers.items()}
return {
"graph_id": self.graph_id,
"batch_size": self.BATCH_SIZE,
"total_activities": self._total_activities, # 添加到队列的活动总数
"batches_sent": self._total_sent, # 成功发送的批次数
"items_sent": self._total_items_sent, # 成功发送的活动条数
"failed_count": self._failed_count, # 发送失败的批次数
"pending_episode_count": len(self._pending_episode_uuids),
"skipped_count": self._skipped_count, # 被过滤跳过的活动数DO_NOTHING
"queue_size": self._activity_queue.qsize(),
"buffer_sizes": buffer_sizes, # 各平台缓冲区大小
"running": self._running,
}
class ZepGraphMemoryManager:
"""
管理多个模拟的Zep图谱记忆更新器
每个模拟可以有自己的更新器实例
"""
_updaters: Dict[str, ZepGraphMemoryUpdater] = {}
_lock = threading.Lock()
@classmethod
def create_updater(cls, simulation_id: str, graph_id: str) -> ZepGraphMemoryUpdater:
"""
为模拟创建图谱记忆更新器
Args:
simulation_id: 模拟ID
graph_id: Zep图谱ID
Returns:
ZepGraphMemoryUpdater实例
"""
with cls._lock:
# 如果已存在,先停止旧的
if simulation_id in cls._updaters:
cls._updaters[simulation_id].stop()
updater = ZepGraphMemoryUpdater(
graph_id,
simulation_id=simulation_id,
)
updater.start()
cls._updaters[simulation_id] = updater
cls._stop_all_done = False
logger.info(f"创建图谱记忆更新器: simulation_id={simulation_id}, graph_id={graph_id}")
return updater
@classmethod
def get_updater(cls, simulation_id: str) -> Optional[ZepGraphMemoryUpdater]:
"""获取模拟的更新器"""
with cls._lock:
return cls._updaters.get(simulation_id)
@classmethod
def get_simulation_ids_for_graph(cls, graph_id: str) -> List[str]:
"""Return simulations whose updater still owns or drains this graph."""
with cls._lock:
return sorted(
simulation_id
for simulation_id, updater in cls._updaters.items()
if updater.graph_id == graph_id
)
@classmethod
def get_simulation_ids(cls) -> List[str]:
"""Return every simulation with a retained updater."""
with cls._lock:
return sorted(cls._updaters)
@classmethod
def discard_inactive_updater(cls, simulation_id: str) -> bool:
"""Discard a failed, fully stopped updater during graph destruction."""
with cls._lock:
updater = cls._updaters.get(simulation_id)
if updater is None:
return False
worker_alive = bool(
updater._worker_thread and updater._worker_thread.is_alive()
)
if updater._running or worker_alive:
raise RuntimeError(
f"Zep updater for {simulation_id} is still active"
)
cls._updaters.pop(simulation_id, None)
logger.warning(
"Discarded incomplete Zep updater during explicit graph deletion: "
"simulation_id=%s, graph_id=%s",
simulation_id,
updater.graph_id,
)
return True
@classmethod
def stop_updater(cls, simulation_id: str):
"""停止并移除模拟的更新器"""
with cls._lock:
updater = cls._updaters.get(simulation_id)
if updater is None:
return
# Do not hold the manager lock through up to several minutes of Cloud
# polling. Crucially, only remove the updater after a successful drain;
# on failure it remains visible to report/deletion barriers and can be
# stopped again.
updater.stop()
with cls._lock:
if cls._updaters.get(simulation_id) is updater:
cls._updaters.pop(simulation_id, None)
logger.info(f"已停止图谱记忆更新器: simulation_id={simulation_id}")
# 防止 stop_all 重复调用的标志
_stop_all_done = False
@classmethod
def stop_all(cls):
"""停止所有更新器"""
# 防止重复调用
if cls._stop_all_done:
return
with cls._lock:
simulation_ids = list(cls._updaters)
errors = []
for simulation_id in simulation_ids:
try:
cls.stop_updater(simulation_id)
except Exception as error:
# Keep a failed updater registered so the caller can retry and
# lifecycle/report guards still see the incomplete ingestion.
logger.error(
"停止更新器失败: simulation_id=%s, error=%s",
simulation_id,
error,
)
errors.append((simulation_id, error))
with cls._lock:
cls._stop_all_done = not cls._updaters
if errors:
details = "; ".join(
f"{simulation_id}: {error}"
for simulation_id, error in errors
)
raise RuntimeError(f"部分图谱更新器未完整停止: {details}")
logger.info("已停止所有图谱记忆更新器")
@classmethod
def get_all_stats(cls) -> Dict[str, Dict[str, Any]]:
"""获取所有更新器的统计信息"""
return {
sim_id: updater.get_stats()
for sim_id, updater in cls._updaters.items()
}

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,16 @@
"""
工具模块
Utilities module
"""
from .file_parser import FileParser
from .llm_client import LLMClient
from .locale import t, get_locale, set_locale, get_language_instruction
__all__ = ['FileParser', 'LLMClient', 't', 'get_locale', 'set_locale', 'get_language_instruction']
__all__ = [
"FileParser",
"LLMClient",
"t",
"get_locale",
"set_locale",
"get_language_instruction",
]

View File

@ -0,0 +1,55 @@
"""
Embeddings utility uses Ollama /v1/embeddings or OpenAI-compatible endpoint.
Falls back to None if embeddings endpoint unavailable.
"""
import json
from typing import List, Optional
from ..config import Config
from ..utils.logger import get_logger
logger = get_logger("mirofish.embeddings")
_embedding_client = None
def _get_client():
global _embedding_client
if _embedding_client is None:
from openai import OpenAI
_embedding_client = OpenAI(
api_key=Config.LLM_API_KEY or "ollama",
base_url=Config.LLM_BASE_URL,
)
return _embedding_client
def embed(text: str, model: Optional[str] = None) -> Optional[List[float]]:
"""Embed text via the configured LLM endpoint. Returns None on failure."""
if not text or not text.strip():
return None
embed_model = model or Config.LLM_MODEL_NAME
try:
client = _get_client()
resp = client.embeddings.create(model=embed_model, input=text[:8000])
if resp.data and len(resp.data) > 0:
return resp.data[0].embedding
except Exception as e:
logger.debug(
f"Embedding failed (non-fatal, keyword search will be used): {str(e)[:100]}"
)
return None
def cosine_similarity(a: List[float], b: List[float]) -> float:
"""Pure-Python cosine similarity. ponytail: O(d) per comparison, fine for small graphs."""
if not a or not b or len(a) != len(b):
return 0.0
dot = sum(x * y for x, y in zip(a, b))
norm_a = sum(x * x for x in a) ** 0.5
norm_b = sum(x * x for x in b) ** 0.5
if norm_a == 0 or norm_b == 0:
return 0.0
return dot / (norm_a * norm_b)

View File

@ -1,68 +1,72 @@
"""
文件解析工具
支持PDFMarkdownTXT文件的文本提取
File parsing utility
Supports text extraction from PDF, Markdown, TXT files
"""
import os
from pathlib import Path
from typing import List, Optional
from .locale import t
def _read_text_with_fallback(file_path: str) -> str:
"""
读取文本文件UTF-8失败时自动探测编码
采用多级回退策略
1. 首先尝试 UTF-8 解码
2. 使用 charset_normalizer 检测编码
3. 回退到 chardet 检测编码
4. 最终使用 UTF-8 + errors='replace' 兜底
Read a text file, automatically detecting encoding when UTF-8 fails.
Uses a multi-level fallback strategy:
1. First try UTF-8 decoding
2. Use charset_normalizer to detect encoding
3. Fall back to chardet to detect encoding
4. Finally use UTF-8 + errors='replace' as a last resort
Args:
file_path: 文件路径
file_path: File path
Returns:
解码后的文本内容
Decoded text content
"""
data = Path(file_path).read_bytes()
# 首先尝试 UTF-8
# First try UTF-8
try:
return data.decode('utf-8')
return data.decode("utf-8")
except UnicodeDecodeError:
pass
# 尝试使用 charset_normalizer 检测编码
# Try using charset_normalizer to detect encoding
encoding = None
try:
from charset_normalizer import from_bytes
best = from_bytes(data).best()
if best and best.encoding:
encoding = best.encoding
except Exception:
pass
# 回退到 chardet
# Fall back to chardet
if not encoding:
try:
import chardet
result = chardet.detect(data)
encoding = result.get('encoding') if result else None
encoding = result.get("encoding") if result else None
except Exception:
pass
# 最终兜底:使用 UTF-8 + replace
# Final fallback: use UTF-8 + replace
if not encoding:
encoding = 'utf-8'
return data.decode(encoding, errors='replace')
encoding = "utf-8"
return data.decode(encoding, errors="replace")
class FileParser:
"""文件解析器"""
SUPPORTED_EXTENSIONS = {'.pdf', '.md', '.markdown', '.txt'}
"""File parser"""
SUPPORTED_EXTENSIONS = {".pdf", ".md", ".markdown", ".txt"}
@classmethod
def is_supported(cls, file_path: str) -> bool:
"""
@ -80,124 +84,134 @@ class FileParser:
@classmethod
def extract_text(cls, file_path: str) -> str:
"""
从文件中提取文本
Extract text from a file
Args:
file_path: 文件路径
file_path: File path
Returns:
提取的文本内容
Extracted text content
"""
path = Path(file_path)
if not path.exists():
raise FileNotFoundError(f"文件不存在: {file_path}")
raise FileNotFoundError(t("api.fileNotFound", path=file_path))
suffix = path.suffix.lower()
if suffix not in cls.SUPPORTED_EXTENSIONS:
raise ValueError(f"不支持的文件格式: {suffix}")
if suffix == '.pdf':
raise ValueError(t("api.unsupportedFileFormat", suffix=suffix))
if suffix == ".pdf":
return cls._extract_from_pdf(file_path)
elif suffix in {'.md', '.markdown'}:
elif suffix in {".md", ".markdown"}:
return cls._extract_from_md(file_path)
elif suffix == '.txt':
elif suffix == ".txt":
return cls._extract_from_txt(file_path)
raise ValueError(f"无法处理的文件格式: {suffix}")
raise ValueError(t("api.unsupportedFileFormatGeneric", suffix=suffix))
@staticmethod
def _extract_from_pdf(file_path: str) -> str:
"""从PDF提取文本"""
"""Extract text from a PDF"""
try:
import fitz # PyMuPDF
except ImportError:
raise ImportError("需要安装PyMuPDF: pip install PyMuPDF")
raise ImportError("PyMuPDF is required: pip install PyMuPDF")
text_parts = []
with fitz.open(file_path) as doc:
for page in doc:
text = page.get_text()
if text.strip():
text_parts.append(text)
return "\n\n".join(text_parts)
@staticmethod
def _extract_from_md(file_path: str) -> str:
"""从Markdown提取文本支持自动编码检测"""
"""Extract text from Markdown, with automatic encoding detection"""
return _read_text_with_fallback(file_path)
@staticmethod
def _extract_from_txt(file_path: str) -> str:
"""从TXT提取文本支持自动编码检测"""
"""Extract text from TXT, with automatic encoding detection"""
return _read_text_with_fallback(file_path)
@classmethod
def extract_from_multiple(cls, file_paths: List[str]) -> str:
"""
从多个文件提取文本并合并
Extract text from multiple files and merge them
Args:
file_paths: 文件路径列表
file_paths: List of file paths
Returns:
合并后的文本
Merged text
"""
all_texts = []
for i, file_path in enumerate(file_paths, 1):
try:
text = cls.extract_text(file_path)
filename = Path(file_path).name
all_texts.append(f"=== 文档 {i}: {filename} ===\n{text}")
all_texts.append(f"=== Document {i}: {filename} ===\n{text}")
except Exception as e:
all_texts.append(f"=== 文档 {i}: {file_path} (提取失败: {str(e)}) ===")
all_texts.append(
f"=== Document {i}: {file_path} (extraction failed: {str(e)}) ==="
)
return "\n\n".join(all_texts)
def split_text_into_chunks(
text: str,
chunk_size: int = 500,
overlap: int = 50
text: str, chunk_size: int = 500, overlap: int = 50
) -> List[str]:
"""
将文本分割成小块
Split text into chunks
Args:
text: 原始文本
chunk_size: 每块的字符数
overlap: 重叠字符数
text: Original text
chunk_size: Number of characters per chunk
overlap: Number of overlapping characters
Returns:
文本块列表
List of text chunks
"""
if len(text) <= chunk_size:
return [text] if text.strip() else []
chunks = []
start = 0
while start < len(text):
end = start + chunk_size
# 尝试在句子边界处分割
# Try to split at sentence boundaries
if end < len(text):
# 查找最近的句子结束符
for sep in ['', '', '', '.\n', '!\n', '?\n', '\n\n', '. ', '! ', '? ']:
# Find the nearest sentence-ending separator
for sep in [
"",
"",
"",
".\n",
"!\n",
"?\n",
"\n\n",
". ",
"! ",
"? ",
]:
last_sep = text[start:end].rfind(sep)
if last_sep != -1 and last_sep > chunk_size * 0.3:
end = start + last_sep + len(sep)
break
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
# 下一个块从重叠位置开始
start = end - overlap if end < len(text) else len(text)
return chunks
# Next chunk starts from the overlap position
start = end - overlap if end < len(text) else len(text)
return chunks

View File

@ -1,6 +1,6 @@
"""
LLM客户端封装
统一使用OpenAI格式调用
LLM client wrapper
Uses OpenAI-compatible API format
"""
import json
@ -9,48 +9,46 @@ from typing import Optional, Dict, Any, List
from openai import OpenAI
from ..config import Config
from ..utils.locale import t
from .openai_chat_compat import create_chat_completion, extract_chat_completion_text
class LLMClient:
"""LLM客户端"""
"""LLM client"""
def __init__(
self,
api_key: Optional[str] = None,
base_url: Optional[str] = None,
model: Optional[str] = None
model: Optional[str] = None,
):
self.api_key = api_key or Config.LLM_API_KEY
self.base_url = base_url or Config.LLM_BASE_URL
self.model = model or Config.LLM_MODEL_NAME
if not self.api_key:
raise ValueError("LLM_API_KEY 未配置")
self.client = OpenAI(
api_key=self.api_key,
base_url=self.base_url
)
raise ValueError(t("api.llmApiKeyMissing"))
self.client = OpenAI(api_key=self.api_key, base_url=self.base_url)
def chat(
self,
messages: List[Dict[str, str]],
temperature: float = 0.7,
max_tokens: int = 4096,
response_format: Optional[Dict] = None
max_tokens: int = 8192,
response_format: Optional[Dict] = None,
) -> str:
"""
发送聊天请求
Send a chat request
Args:
messages: 消息列表
temperature: 温度参数
max_tokens: 最大token数
response_format: 响应格式如JSON模式
messages: List of messages
temperature: Temperature parameter
max_tokens: Maximum number of tokens
response_format: Response format (e.g. JSON mode)
Returns:
模型响应文本
Model response text
"""
response = create_chat_completion(
self.client,
@ -61,41 +59,42 @@ class LLMClient:
response_format=response_format,
)
content = extract_chat_completion_text(response)
# 部分模型如MiniMax M2.5会在content中包含<think>思考内容,需要移除
content = re.sub(r'<think>[\s\S]*?</think>', '', content).strip()
# Some models (e.g. MiniMax M2.5) include thinking content in the content; remove it
content = re.sub(r"<think>[\s\S]*?</think>", "", content).strip()
return content
def chat_json(
self,
messages: List[Dict[str, str]],
temperature: float = 0.3,
max_tokens: int = 4096
max_tokens: int = 8192,
) -> Dict[str, Any]:
"""
发送聊天请求并返回JSON
Send a chat request and return JSON
Args:
messages: 消息列表
temperature: 温度参数
max_tokens: 最大token数
messages: List of messages
temperature: Temperature parameter
max_tokens: Maximum number of tokens
Returns:
解析后的JSON对象
Parsed JSON object
"""
response = self.chat(
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
response_format={"type": "json_object"}
response_format={"type": "json_object"},
)
# 清理markdown代码块标记
# Clean up markdown code block markers
cleaned_response = response.strip()
cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE)
cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response)
cleaned_response = re.sub(
r"^```(?:json)?\s*\n?", "", cleaned_response, flags=re.IGNORECASE
)
cleaned_response = re.sub(r"\n?```\s*$", "", cleaned_response)
cleaned_response = cleaned_response.strip()
try:
return json.loads(cleaned_response)
except json.JSONDecodeError:
raise ValueError(f"LLM返回的JSON格式无效: {cleaned_response}")
raise ValueError(t("api.llmJsonInvalid", response=cleaned_response))

View File

@ -5,18 +5,18 @@ from flask import request, has_request_context
_thread_local = threading.local()
_locales_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'locales')
_locales_dir = os.path.join(os.path.dirname(__file__), "..", "..", "..", "locales")
# Load language registry
with open(os.path.join(_locales_dir, 'languages.json'), 'r', encoding='utf-8') as f:
with open(os.path.join(_locales_dir, "languages.json"), "r", encoding="utf-8") as f:
_languages = json.load(f)
# Load translation files
_translations = {}
for filename in os.listdir(_locales_dir):
if filename.endswith('.json') and filename != 'languages.json':
if filename.endswith(".json") and filename != "languages.json":
locale_name = filename[:-5]
with open(os.path.join(_locales_dir, filename), 'r', encoding='utf-8') as f:
with open(os.path.join(_locales_dir, filename), "r", encoding="utf-8") as f:
_translations[locale_name] = json.load(f)
@ -27,17 +27,17 @@ def set_locale(locale: str):
def get_locale() -> str:
if has_request_context():
raw = request.headers.get('Accept-Language', 'zh')
return raw if raw in _translations else 'zh'
return getattr(_thread_local, 'locale', 'zh')
raw = request.headers.get("Accept-Language", "en")
return raw if raw in _translations else "en"
return getattr(_thread_local, "locale", "en")
def t(key: str, **kwargs) -> str:
locale = get_locale()
messages = _translations.get(locale, _translations.get('zh', {}))
messages = _translations.get(locale, _translations.get("en", {}))
value = messages
for part in key.split('.'):
for part in key.split("."):
if isinstance(value, dict):
value = value.get(part)
else:
@ -45,8 +45,8 @@ def t(key: str, **kwargs) -> str:
break
if value is None:
value = _translations.get('zh', {})
for part in key.split('.'):
value = _translations.get("en", {})
for part in key.split("."):
if isinstance(value, dict):
value = value.get(part)
else:
@ -58,12 +58,12 @@ def t(key: str, **kwargs) -> str:
if kwargs:
for k, v in kwargs.items():
value = value.replace(f'{{{k}}}', str(v))
value = value.replace(f"{{{k}}}", str(v))
return value
def get_language_instruction() -> str:
locale = get_locale()
lang_config = _languages.get(locale, _languages.get('zh', {}))
return lang_config.get('llmInstruction', '请使用中文回答。')
lang_config = _languages.get(locale, _languages.get("en", {}))
return lang_config.get("llmInstruction", "Please respond in English.")

View File

@ -1,6 +1,6 @@
"""
日志配置模块
提供统一的日志管理同时输出到控制台和文件
Logging configuration module
Provides unified log management, outputting to both console and file
"""
import os
@ -12,91 +12,92 @@ from logging.handlers import RotatingFileHandler
def _ensure_utf8_stdout():
"""
确保 stdout/stderr 使用 UTF-8 编码
解决 Windows 控制台中文乱码问题
Ensure stdout/stderr use UTF-8 encoding
Fixes garbled Chinese output in the Windows console
"""
if sys.platform == 'win32':
# Windows 下重新配置标准输出为 UTF-8
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
if sys.platform == "win32":
# Reconfigure standard output to UTF-8 on Windows
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
# 日志目录
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'logs')
# Log directory
LOG_DIR = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "logs"
)
def setup_logger(name: str = 'mirofish', level: int = logging.DEBUG) -> logging.Logger:
def setup_logger(name: str = "mirofish", level: int = logging.DEBUG) -> logging.Logger:
"""
设置日志器
Set up a logger
Args:
name: 日志器名称
level: 日志级别
name: Logger name
level: Log level
Returns:
配置好的日志器
Configured logger
"""
# 确保日志目录存在
# Ensure log directory exists
os.makedirs(LOG_DIR, exist_ok=True)
# 创建日志器
# Create logger
logger = logging.getLogger(name)
logger.setLevel(level)
# 阻止日志向上传播到根 logger避免重复输出
# Prevent logs from propagating to the root logger to avoid duplicate output
logger.propagate = False
# 如果已经有处理器,不重复添加
# If handlers already exist, do not add duplicates
if logger.handlers:
return logger
# 日志格式
# Log format
detailed_formatter = logging.Formatter(
'[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
"[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
simple_formatter = logging.Formatter(
'[%(asctime)s] %(levelname)s: %(message)s',
datefmt='%H:%M:%S'
"[%(asctime)s] %(levelname)s: %(message)s", datefmt="%H:%M:%S"
)
# 1. 文件处理器 - 详细日志(按日期命名,带轮转)
log_filename = datetime.now().strftime('%Y-%m-%d') + '.log'
# 1. File handler - detailed logs (named by date, with rotation)
log_filename = datetime.now().strftime("%Y-%m-%d") + ".log"
file_handler = RotatingFileHandler(
os.path.join(LOG_DIR, log_filename),
maxBytes=10 * 1024 * 1024, # 10MB
backupCount=5,
encoding='utf-8'
encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(detailed_formatter)
# 2. 控制台处理器 - 简洁日志INFO及以上
# 确保 Windows 下使用 UTF-8 编码,避免中文乱码
# 2. Console handler - concise logs (INFO and above)
# Ensure UTF-8 encoding on Windows to avoid garbled Chinese characters
_ensure_utf8_stdout()
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(simple_formatter)
# 添加处理器
# Add handlers
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger
def get_logger(name: str = 'mirofish') -> logging.Logger:
def get_logger(name: str = "mirofish") -> logging.Logger:
"""
获取日志器如果不存在则创建
Get a logger (create one if it does not exist)
Args:
name: 日志器名称
name: Logger name
Returns:
日志器实例
Logger instance
"""
logger = logging.getLogger(name)
if not logger.handlers:
@ -104,23 +105,26 @@ def get_logger(name: str = 'mirofish') -> logging.Logger:
return logger
# 创建默认日志器
# Create default logger
logger = setup_logger()
# 便捷方法
def debug(msg: str, *args, **kwargs) -> None:
# Convenience methods
def debug(msg, *args, **kwargs):
logger.debug(msg, *args, **kwargs)
def info(msg: str, *args, **kwargs) -> None:
def info(msg, *args, **kwargs):
logger.info(msg, *args, **kwargs)
def warning(msg: str, *args, **kwargs) -> None:
def warning(msg, *args, **kwargs):
logger.warning(msg, *args, **kwargs)
def error(msg: str, *args, **kwargs) -> None:
def error(msg, *args, **kwargs):
logger.error(msg, *args, **kwargs)
def critical(msg: str, *args, **kwargs) -> None:
logger.critical(msg, *args, **kwargs)
def critical(msg, *args, **kwargs):
logger.critical(msg, *args, **kwargs)

View File

@ -1,6 +1,6 @@
"""
API调用重试机制
用于处理LLM等外部API调用的重试逻辑
API call retry mechanism
Used to handle retry logic for external API calls such as LLM
"""
import time
@ -9,7 +9,7 @@ import functools
from typing import Callable, Any, Optional, Type, Tuple
from ..utils.logger import get_logger
logger = get_logger('mirofish.retry')
logger = get_logger("mirofish.retry")
def retry_with_backoff(
@ -19,61 +19,65 @@ def retry_with_backoff(
backoff_factor: float = 2.0,
jitter: bool = True,
exceptions: Tuple[Type[Exception], ...] = (Exception,),
on_retry: Optional[Callable[[Exception, int], None]] = None
on_retry: Optional[Callable[[Exception, int], None]] = None,
):
"""
带指数退避的重试装饰器
Retry decorator with exponential backoff
Args:
max_retries: 最大重试次数
initial_delay: 初始延迟
max_delay: 最大延迟
backoff_factor: 退避因子
jitter: 是否添加随机抖动
exceptions: 需要重试的异常类型
on_retry: 重试时的回调函数 (exception, retry_count)
max_retries: Maximum number of retries
initial_delay: Initial delay (seconds)
max_delay: Maximum delay (seconds)
backoff_factor: Backoff factor
jitter: Whether to add random jitter
exceptions: Exception types to retry on
on_retry: Retry callback function (exception, retry_count)
Usage:
@retry_with_backoff(max_retries=3)
def call_llm_api():
...
"""
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
def wrapper(*args, **kwargs) -> Any:
last_exception = None
delay = initial_delay
for attempt in range(max_retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt == max_retries:
logger.error(f"函数 {func.__name__}{max_retries} 次重试后仍失败: {str(e)}")
logger.error(
f"Function {func.__name__} still failed after {max_retries} retries: {str(e)}"
)
raise
# 计算延迟
# Calculate delay
current_delay = min(delay, max_delay)
if jitter:
current_delay = current_delay * (0.5 + random.random())
logger.warning(
f"函数 {func.__name__}{attempt + 1} 次尝试失败: {str(e)}, "
f"{current_delay:.1f}秒后重试..."
f"Function {func.__name__} attempt {attempt + 1} failed: {str(e)}, "
f"retrying in {current_delay:.1f}s..."
)
if on_retry:
on_retry(e, attempt + 1)
time.sleep(current_delay)
delay *= backoff_factor
raise last_exception
return wrapper
return decorator
@ -84,155 +88,151 @@ def retry_with_backoff_async(
backoff_factor: float = 2.0,
jitter: bool = True,
exceptions: Tuple[Type[Exception], ...] = (Exception,),
on_retry: Optional[Callable[[Exception, int], None]] = None
on_retry: Optional[Callable[[Exception, int], None]] = None,
):
"""
异步版本的重试装饰器
Async version of the retry decorator
"""
import asyncio
def decorator(func: Callable) -> Callable:
@functools.wraps(func)
async def wrapper(*args, **kwargs) -> Any:
last_exception = None
delay = initial_delay
for attempt in range(max_retries + 1):
try:
return await func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt == max_retries:
logger.error(f"异步函数 {func.__name__}{max_retries} 次重试后仍失败: {str(e)}")
logger.error(
f"Async function {func.__name__} still failed after {max_retries} retries: {str(e)}"
)
raise
current_delay = min(delay, max_delay)
if jitter:
current_delay = current_delay * (0.5 + random.random())
logger.warning(
f"异步函数 {func.__name__}{attempt + 1} 次尝试失败: {str(e)}, "
f"{current_delay:.1f}秒后重试..."
f"Async function {func.__name__} attempt {attempt + 1} failed: {str(e)}, "
f"retrying in {current_delay:.1f}s..."
)
if on_retry:
on_retry(e, attempt + 1)
await asyncio.sleep(current_delay)
delay *= backoff_factor
raise last_exception
return wrapper
return decorator
class RetryableAPIClient:
"""
可重试的API客户端封装
Retryable API client wrapper
"""
def __init__(
self,
max_retries: int = 3,
initial_delay: float = 1.0,
max_delay: float = 30.0,
backoff_factor: float = 2.0
backoff_factor: float = 2.0,
):
self.max_retries = max_retries
self.initial_delay = initial_delay
self.max_delay = max_delay
self.backoff_factor = backoff_factor
def call_with_retry(
self,
func: Callable,
*args,
exceptions: Tuple[Type[Exception], ...] = (Exception,),
**kwargs
**kwargs,
) -> Any:
"""
执行函数调用并在失败时重试
Execute a function call and retry on failure
Args:
func: 要调用的函数
*args: 函数参数
exceptions: 需要重试的异常类型
**kwargs: 函数关键字参数
func: The function to call
*args: Function arguments
exceptions: Exception types to retry on
**kwargs: Function keyword arguments
Returns:
函数返回值
Function return value
"""
last_exception = None
delay = self.initial_delay
for attempt in range(self.max_retries + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
last_exception = e
if attempt == self.max_retries:
logger.error(f"API调用在 {self.max_retries} 次重试后仍失败: {str(e)}")
logger.error(
f"API call still failed after {self.max_retries} retries: {str(e)}"
)
raise
current_delay = min(delay, self.max_delay)
current_delay = current_delay * (0.5 + random.random())
logger.warning(
f"API调用第 {attempt + 1} 次尝试失败: {str(e)}, "
f"{current_delay:.1f}秒后重试..."
f"API call attempt {attempt + 1} failed: {str(e)}, "
f"retrying in {current_delay:.1f}s..."
)
time.sleep(current_delay)
delay *= self.backoff_factor
raise last_exception
def call_batch_with_retry(
self,
items: list,
process_func: Callable,
exceptions: Tuple[Type[Exception], ...] = (Exception,),
continue_on_failure: bool = True
continue_on_failure: bool = True,
) -> Tuple[list, list]:
"""
批量调用并对每个失败项单独重试
Batch call with individual retry for each failed item
Args:
items: 要处理的项目列表
process_func: 处理函数接收单个item作为参数
exceptions: 需要重试的异常类型
continue_on_failure: 单项失败后是否继续处理其他项
items: List of items to process
process_func: Processing function, takes a single item as argument
exceptions: Exception types to retry on
continue_on_failure: Whether to continue processing other items after one fails
Returns:
(成功结果列表, 失败项列表)
(list of successful results, list of failed items)
"""
results = []
failures = []
for idx, item in enumerate(items):
try:
result = self.call_with_retry(
process_func,
item,
exceptions=exceptions
)
result = self.call_with_retry(process_func, item, exceptions=exceptions)
results.append(result)
except Exception as e:
logger.error(f"处理第 {idx + 1} 项失败: {str(e)}")
failures.append({
"index": idx,
"item": item,
"error": str(e)
})
logger.error(f"Failed to process item {idx + 1}: {str(e)}")
failures.append({"index": idx, "item": item, "error": str(e)})
if not continue_on_failure:
raise
return results, failures
return results, failures

View File

@ -1,160 +0,0 @@
"""Complete Zep Graph node/edge pagination using opaque response cursors."""
from __future__ import annotations
from collections.abc import Callable
from typing import Any
from zep_cloud.client import Zep
from .logger import get_logger
from .zep import call_zep_read_with_retry
logger = get_logger("mirofish.zep_paging")
_DEFAULT_PAGE_SIZE = 100
_DEFAULT_MAX_RETRIES = 3
_DEFAULT_RETRY_DELAY = 2.0
_NEXT_CURSOR_HEADER = "zep-next-cursor"
def _fetch_page_with_retry(
api_call: Callable[..., Any],
*args: Any,
max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY,
page_description: str = "page",
**kwargs: Any,
) -> Any:
"""Fetch one read-only page with the shared transient-error policy."""
return call_zep_read_with_retry(
lambda: api_call(*args, **kwargs),
operation_name=page_description,
max_attempts=max_retries,
initial_delay=retry_delay,
)
def _header_value(headers: Any, name: str) -> str | None:
if not headers:
return None
direct = headers.get(name)
if direct is not None:
return str(direct)
return next(
(
str(value)
for header_name, value in headers.items()
if str(header_name).lower() == name.lower()
),
None,
)
def _fetch_all(
api_call: Callable[..., Any],
graph_id: str,
*,
item_name: str,
page_size: int,
max_items: int | None,
max_retries: int,
retry_delay: float,
) -> list[Any]:
if not 1 <= page_size <= 100:
raise ValueError("page_size must be between 1 and 100")
if max_items is not None and max_items < 1:
raise ValueError("max_items must be at least 1 when provided")
all_items: list[Any] = []
cursor: str | None = None
seen_cursors: set[str] = set()
page_number = 0
while True:
kwargs: dict[str, Any] = {"limit": page_size}
if cursor is not None:
kwargs["cursor"] = cursor
page_number += 1
response = _fetch_page_with_retry(
api_call,
graph_id,
max_retries=max_retries,
retry_delay=retry_delay,
page_description=(
f"fetch {item_name} page {page_number} (graph={graph_id})"
),
**kwargs,
)
batch = list(getattr(response, "data", None) or [])
all_items.extend(batch)
if max_items is not None and len(all_items) >= max_items:
if len(all_items) > max_items:
all_items = all_items[:max_items]
logger.warning(
"Zep %s pagination reached explicit max_items=%s for graph %s",
item_name,
max_items,
graph_id,
)
break
next_cursor = _header_value(
getattr(response, "headers", None),
_NEXT_CURSOR_HEADER,
)
if next_cursor is None:
break
if next_cursor in seen_cursors or next_cursor == cursor:
raise RuntimeError(
f"Zep {item_name} pagination cursor did not advance for graph {graph_id}"
)
seen_cursors.add(next_cursor)
cursor = next_cursor
return all_items
def fetch_all_nodes(
client: Zep,
graph_id: str,
page_size: int = _DEFAULT_PAGE_SIZE,
max_items: int | None = None,
max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY,
) -> list[Any]:
"""Fetch every graph node unless the caller supplies an explicit cap."""
return _fetch_all(
client.graph.node.with_raw_response.get_by_graph_id,
graph_id,
item_name="nodes",
page_size=page_size,
max_items=max_items,
max_retries=max_retries,
retry_delay=retry_delay,
)
def fetch_all_edges(
client: Zep,
graph_id: str,
page_size: int = _DEFAULT_PAGE_SIZE,
max_retries: int = _DEFAULT_MAX_RETRIES,
retry_delay: float = _DEFAULT_RETRY_DELAY,
max_items: int | None = None,
) -> list[Any]:
"""Fetch every graph edge unless the caller supplies an explicit cap."""
return _fetch_all(
client.graph.edge.with_raw_response.get_by_graph_id,
graph_id,
item_name="edges",
page_size=page_size,
max_items=max_items,
max_retries=max_retries,
retry_delay=retry_delay,
)

View File

@ -0,0 +1,127 @@
# Nedbank Africa Expansion — Public Sentiment Prediction
> **MiroFish Simulation Requirement Preset**
> Version: v1.0.0 | Created: 2026-07-10
---
## Project Name
Nedbank Africa Expansion Sentiment Prediction
## Simulation Requirement
Nedbank, one of South Africa's largest retail banks, announces a strategic
expansion into the rest of the African continent, opening branches and digital
banking services in Nigeria, Kenya, Ghana, and Mozambique over the next 18
months. The expansion includes partnerships with local telecom providers for
mobile money integration, a US$200 million investment fund, and a marketing
campaign positioning Nedbank as "the bank that understands Africa." Predict the
public sentiment response across social media platforms (Twitter and Reddit) in
the days following the announcement. Consider the perspectives of South African
customers reacting to capital outflow concerns, Nigerian and Kenyan citizens'
reception of a South African bank entering their markets, fintech competitors
(like MTN MoMo, Flutterwave, OPay), financial analysts discussing the investment
rationale, regulators in each target country, pan-African economic nationalists,
and ordinary citizens concerned about data privacy and neo-colonial financial
dominance. How does sentiment evolve over a 72-hour period across these diverse
stakeholder groups?
## Additional Context
Key factors to model:
1. South-South investment dynamics and historical skepticism toward South African corporate expansion in Africa
2. Competitive landscape with established fintech players (Flutterwave, OPay, Paystack, MPesa)
3. Mobile money integration as a differentiator
4. Regulatory uncertainty in each target market
5. Currency volatility and capital controls concerns from SA depositors
6. Nedbank's ESG commitments and "Africa-focused" brand positioning
7. Data sovereignty concerns in target countries
8. Comparison to prior pan-African bank expansions (Standard Bank, Absa/Barclays)
The simulation should capture the initial announcement hype, analyst
commentary, competitive response, regulatory statements, and citizen reactions
across geographic and demographic segments.
## Recommended Entity Types
Suggested agent personas for the knowledge graph:
| Entity Type | Description |
|---|---|
| Customer | General retail banking customer |
| BankingCustomer | Nedbank-specific SA customer |
| FintechCompetitor | Competitor fintech (Flutterwave, OPay, Paystack, MPesa, MTN MoMo) |
| FinancialAnalyst | Market analyst covering African banking |
| Regulator | Financial regulator in target country |
| Journalist | Business/tech journalist covering the story |
| EconomicNationalist | Pan-African economic sovereignty advocate |
| Citizen | Ordinary citizen in a target market |
| TelecomPartner | Local telecom partner providing mobile money rails |
| GovernmentOfficial | Government official in target country |
| Investor | Nedbank shareholder / institutional investor |
| SocialMediaInfluencer | Influencer shaping public discourse |
## Recommended Platforms
| Platform | Enabled |
|---|---|
| Twitter | Yes |
| Reddit | Yes |
## Recommended Time Configuration
| Parameter | Value |
|---|---|
| Total simulation hours | 72 |
| Minutes per round | 30 |
| Approx. total rounds | 144 |
## Suggested Source Documents
Upload these (or similar) documents when creating the MiroFish project so the
ontology generator and knowledge graph have rich source material:
1. Nedbank annual report and Africa strategy disclosure (PDF)
2. Press release on the expansion announcement (MD)
3. Market analysis reports on African banking sector (PDF/MD)
4. Articles on Standard Bank and Absa prior pan-African expansion outcomes (MD)
5. Fintech landscape reports for Nigeria, Kenya, Ghana, Mozambique (MD)
6. Social media sentiment studies on South African brands entering African markets (TXT)
## How to Use This Preset
1. **Create project** — In the MiroFish frontend, enter the **Project Name**
above, paste the **Simulation Requirement** and **Additional Context** into
the project creation form.
2. **Upload documents** — Upload the suggested source documents (or equivalent
real-world materials).
3. **Generate ontology** — The pipeline auto-generates entity/edge types from
the documents and requirement.
4. **Build graph** — The knowledge graph is constructed from the ontology.
5. **Generate profiles** — Agent personas are created for each entity in the
graph.
6. **Run simulation** — Twitter + Reddit simulation runs for 72 simulated hours.
7. **Generate report** — The Report Agent produces a sentiment prediction report
across all stakeholder groups.
## Usage via API
```bash
# Create project with ontology generation (Endpoint 1)
curl -X POST http://localhost:5001/api/graph/ontology/generate \
-F "project_name=Nedbank Africa Expansion Sentiment Prediction" \
-F "simulation_requirement=Nedbank, one of South Africa's largest retail banks, announces a strategic expansion into the rest of the African continent, opening branches and digital banking services in Nigeria, Kenya, Ghana, and Mozambique over the next 18 months. The expansion includes partnerships with local telecom providers for mobile money integration, a US\$200 million investment fund, and a marketing campaign positioning Nedbank as 'the bank that understands Africa.' Predict the public sentiment response across social media platforms (Twitter and Reddit) in the days following the announcement. Consider the perspectives of South African customers reacting to capital outflow concerns, Nigerian and Kenyan citizens' reception of a South African bank entering their markets, fintech competitors (like MTN MoMo, Flutterwave, OPay), financial analysts discussing the investment rationale, regulators in each target country, pan-African economic nationalists, and ordinary citizens concerned about data privacy and neo-colonial financial dominance. How does sentiment evolve over a 72-hour period across these diverse stakeholder groups?" \
-F "additional_context=Key factors to model: (1) South-South investment dynamics and historical skepticism toward South African corporate expansion in Africa; (2) Competitive landscape with established fintech players (Flutterwave, OPay, Paystack, MPesa); (3) Mobile money integration as a differentiator; (4) Regulatory uncertainty in each target market; (5) Currency volatility and capital controls concerns from SA depositors; (6) Nedbank's ESG commitments and 'Africa-focused' brand positioning; (7) Data sovereignty concerns in target countries; (8) Comparison to prior pan-African bank expansions (Standard Bank, Absa/Barclays). The simulation should capture the initial announcement hype, analyst commentary, competitive response, regulatory statements, and citizen reactions across geographic and demographic segments." \
-F "files=@nedbank_annual_report.pdf" \
-F "files=@africa_banking_market_analysis.md" \
-F "files=@fintech_landscape_west_africa.md"
# The response contains project_id — use it for subsequent steps:
# 2. Build graph: POST /api/graph/build
# 3. Create sim: POST /api/simulation/create
# 4. Prepare sim: POST /api/simulation/prepare
# 5. Start sim: POST /api/simulation/start
# 6. Generate report: POST /api/report/generate
```

View File

@ -1,36 +1,32 @@
[project]
name = "mirofish-backend"
version = "0.1.0"
description = "MiroFish - 简洁通用的群体智能引擎,预测万物"
requires-python = ">=3.11,<3.13"
description = "MiroFish - A concise general-purpose collective intelligence engine for predicting anything"
requires-python = ">=3.11"
license = { text = "AGPL-3.0" }
authors = [
{ name = "MiroFish Team" }
]
dependencies = [
# 核心框架
# Core framework
"flask>=3.0.0",
"flask-cors>=6.0.0",
# LLM 相关
# LLM related
"openai>=1.0.0",
# Zep Cloud
"zep-cloud==3.25.0",
"httpx>=0.27.0",
# OASIS 社交媒体模拟
# OASIS social media simulation
"camel-oasis==0.2.5",
"camel-ai==0.2.78",
# 文件处理
# File processing
"PyMuPDF>=1.24.0",
# 编码检测支持非UTF-8编码的文本文件
# Encoding detection (supports non-UTF-8 encoded text files)
"charset-normalizer>=3.0.0",
"chardet>=5.0.0",
# 工具库
# Utility libraries
"python-dotenv>=1.0.0",
"pydantic>=2.0.0",
]
@ -53,4 +49,4 @@ dev = [
]
[tool.hatch.build.targets.wheel]
packages = ["app"]
packages = ["app"]

View File

@ -5,32 +5,28 @@
# Install: pip install -r requirements.txt
# ===========================================
# ============= 核心框架 =============
# ============= Core Framework =============
flask>=3.0.0
flask-cors>=6.0.0
# ============= LLM 相关 =============
# OpenAI SDK(统一使用 OpenAI 格式调用 LLM
# ============= LLM Related =============
# OpenAI SDK (unified OpenAI format for LLM calls)
openai>=1.0.0
# ============= Zep Cloud =============
zep-cloud==3.25.0
httpx>=0.27.0
# ============= OASIS 社交媒体模拟 =============
# OASIS 社交模拟框架
# ============= OASIS Social Media Simulation =============
# OASIS social simulation framework
camel-oasis==0.2.5
camel-ai==0.2.78
# ============= 文件处理 =============
# ============= File Processing =============
PyMuPDF>=1.24.0
# 编码检测支持非UTF-8编码的文本文件
# Encoding detection (supports non-UTF-8 encoded text files)
charset-normalizer>=3.0.0
chardet>=5.0.0
# ============= 工具库 =============
# 环境变量加载
# ============= Utility Libraries =============
# Environment variable loading
python-dotenv>=1.0.0
# 数据验证
pydantic>=2.0.0
# Data validation
pydantic>=2.0.0

View File

@ -1,21 +1,21 @@
"""
MiroFish Backend 启动入口
MiroFish Backend entry point
"""
import os
import sys
# 解决 Windows 控制台中文乱码问题:在所有导入之前设置 UTF-8 编码
if sys.platform == 'win32':
# 设置环境变量确保 Python 使用 UTF-8
os.environ.setdefault('PYTHONIOENCODING', 'utf-8')
# 重新配置标准输出流为 UTF-8
if hasattr(sys.stdout, 'reconfigure'):
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if hasattr(sys.stderr, 'reconfigure'):
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
# Fix Chinese garbled output in Windows console: set UTF-8 encoding before all imports
if sys.platform == "win32":
# Set environment variable to ensure Python uses UTF-8
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
# Reconfigure standard output streams to UTF-8
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
# 添加项目根目录到路径
# Add project root directory to path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import create_app
@ -23,28 +23,27 @@ from app.config import Config
def main():
"""主函数"""
# 验证配置
"""Main function"""
# Validate configuration
errors = Config.validate()
if errors:
print("配置错误:")
print("Configuration errors:")
for err in errors:
print(f" - {err}")
print("\n请检查 .env 文件中的配置")
print("\nPlease check the configuration in the .env file")
sys.exit(1)
# 创建应用
# Create application
app = create_app()
# 获取运行配置
host = os.environ.get('FLASK_HOST', '0.0.0.0')
port = int(os.environ.get('FLASK_PORT', 5001))
# Get runtime config
host = os.environ.get("FLASK_HOST", "0.0.0.0")
port = int(os.environ.get("FLASK_PORT", 5001))
debug = Config.DEBUG
# 启动服务
# Start service
app.run(host=host, port=port, debug=debug, threaded=True)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@ -1,15 +1,15 @@
"""
动作日志记录器
用于记录OASIS模拟中每个Agent的动作供后端监控使用
Action logger.
Used to log each agent's actions in OASIS simulations for backend monitoring.
日志结构:
Log structure:
sim_xxx/
twitter/
actions.jsonl # Twitter 平台动作日志
actions.jsonl # Twitter platform action log
reddit/
actions.jsonl # Reddit 平台动作日志
simulation.log # 主模拟进程日志
run_state.json # 运行状态API 查询用)
actions.jsonl # Reddit platform action log
simulation.log # Main simulation process log
run_state.json # Run state (for API queries)
"""
import json
@ -20,26 +20,26 @@ from typing import Dict, Any, Optional
class PlatformActionLogger:
"""单平台动作日志记录器"""
"""Single-platform action logger"""
def __init__(self, platform: str, base_dir: str):
"""
初始化日志记录器
Initialize the logger.
Args:
platform: 平台名称 (twitter/reddit)
base_dir: 模拟目录的基础路径
platform: Platform name (twitter/reddit)
base_dir: Base path of the simulation directory
"""
self.platform = platform
self.base_dir = base_dir
self.log_dir = os.path.join(base_dir, platform)
self.log_path = os.path.join(self.log_dir, "actions.jsonl")
self._ensure_dir()
def _ensure_dir(self):
"""确保目录存在"""
"""Ensure the directory exists"""
os.makedirs(self.log_dir, exist_ok=True)
def log_action(
self,
round_num: int,
@ -48,9 +48,9 @@ class PlatformActionLogger:
action_type: str,
action_args: Optional[Dict[str, Any]] = None,
result: Optional[str] = None,
success: bool = True
success: bool = True,
):
"""记录一个动作"""
"""Log an action"""
entry = {
"round": round_num,
"timestamp": datetime.now().isoformat(),
@ -61,49 +61,52 @@ class PlatformActionLogger:
"result": result,
"success": success,
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_round_start(self, round_num: int, simulated_hour: int):
"""记录轮次开始"""
"""Log round start"""
entry = {
"round": round_num,
"timestamp": datetime.now().isoformat(),
"event_type": "round_start",
"simulated_hour": simulated_hour,
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_round_end(self, round_num: int, actions_count: int):
"""记录轮次结束"""
"""Log round end"""
entry = {
"round": round_num,
"timestamp": datetime.now().isoformat(),
"event_type": "round_end",
"actions_count": actions_count,
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_simulation_start(self, config: Dict[str, Any]):
"""记录模拟开始"""
"""Log simulation start"""
entry = {
"timestamp": datetime.now().isoformat(),
"event_type": "simulation_start",
"platform": self.platform,
"total_rounds": config.get("time_config", {}).get("total_simulation_hours", 72) * 2,
"total_rounds": config.get("time_config", {}).get(
"total_simulation_hours", 72
)
* 2,
"agents_count": len(config.get("agent_configs", [])),
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_simulation_end(self, total_rounds: int, total_actions: int):
"""记录模拟结束"""
"""Log simulation end"""
entry = {
"timestamp": datetime.now().isoformat(),
"event_type": "simulation_end",
@ -111,108 +114,111 @@ class PlatformActionLogger:
"total_rounds": total_rounds,
"total_actions": total_actions,
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
class SimulationLogManager:
"""
模拟日志管理器
统一管理所有日志文件按平台分离
Simulation log manager.
Unified management of all log files, separated by platform.
"""
def __init__(self, simulation_dir: str):
"""
初始化日志管理器
Initialize the log manager.
Args:
simulation_dir: 模拟目录路径
simulation_dir: Simulation directory path
"""
self.simulation_dir = simulation_dir
self.twitter_logger: Optional[PlatformActionLogger] = None
self.reddit_logger: Optional[PlatformActionLogger] = None
self._main_logger: Optional[logging.Logger] = None
# 设置主日志
# Set up main log
self._setup_main_logger()
def _setup_main_logger(self):
"""设置主模拟日志"""
"""Set up the main simulation log"""
log_path = os.path.join(self.simulation_dir, "simulation.log")
# 创建 logger
self._main_logger = logging.getLogger(f"simulation.{os.path.basename(self.simulation_dir)}")
# Create logger
self._main_logger = logging.getLogger(
f"simulation.{os.path.basename(self.simulation_dir)}"
)
self._main_logger.setLevel(logging.INFO)
self._main_logger.handlers.clear()
# 文件处理器
file_handler = logging.FileHandler(log_path, encoding='utf-8', mode='w')
# File handler
file_handler = logging.FileHandler(log_path, encoding="utf-8", mode="w")
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
))
file_handler.setFormatter(
logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
)
self._main_logger.addHandler(file_handler)
# 控制台处理器
# Console handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(
'[%(asctime)s] %(message)s',
datefmt='%H:%M:%S'
))
console_handler.setFormatter(
logging.Formatter("[%(asctime)s] %(message)s", datefmt="%H:%M:%S")
)
self._main_logger.addHandler(console_handler)
self._main_logger.propagate = False
def get_twitter_logger(self) -> PlatformActionLogger:
"""获取 Twitter 平台日志记录器"""
"""Get the Twitter platform logger"""
if self.twitter_logger is None:
self.twitter_logger = PlatformActionLogger("twitter", self.simulation_dir)
return self.twitter_logger
def get_reddit_logger(self) -> PlatformActionLogger:
"""获取 Reddit 平台日志记录器"""
"""Get the Reddit platform logger"""
if self.reddit_logger is None:
self.reddit_logger = PlatformActionLogger("reddit", self.simulation_dir)
return self.reddit_logger
def log(self, message: str, level: str = "info"):
"""记录主日志"""
"""Log a main log message"""
if self._main_logger:
getattr(self._main_logger, level.lower(), self._main_logger.info)(message)
def info(self, message: str):
self.log(message, "info")
def warning(self, message: str):
self.log(message, "warning")
def error(self, message: str):
self.log(message, "error")
def debug(self, message: str):
self.log(message, "debug")
# ============ 兼容旧接口 ============
# ============ Legacy interface compatibility ============
class ActionLogger:
"""
动作日志记录器兼容旧接口
建议使用 SimulationLogManager 代替
Action logger (legacy interface compatible).
Using SimulationLogManager is recommended instead.
"""
def __init__(self, log_path: str):
self.log_path = log_path
self._ensure_dir()
def _ensure_dir(self):
log_dir = os.path.dirname(self.log_path)
if log_dir:
os.makedirs(log_dir, exist_ok=True)
def log_action(
self,
round_num: int,
@ -222,7 +228,7 @@ class ActionLogger:
action_type: str,
action_args: Optional[Dict[str, Any]] = None,
result: Optional[str] = None,
success: bool = True
success: bool = True,
):
entry = {
"round": round_num,
@ -235,10 +241,10 @@ class ActionLogger:
"result": result,
"success": success,
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_round_start(self, round_num: int, simulated_hour: int, platform: str):
entry = {
"round": round_num,
@ -247,10 +253,10 @@ class ActionLogger:
"event_type": "round_start",
"simulated_hour": simulated_hour,
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_round_end(self, round_num: int, actions_count: int, platform: str):
entry = {
"round": round_num,
@ -259,22 +265,25 @@ class ActionLogger:
"event_type": "round_end",
"actions_count": actions_count,
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_simulation_start(self, platform: str, config: Dict[str, Any]):
entry = {
"timestamp": datetime.now().isoformat(),
"platform": platform,
"event_type": "simulation_start",
"total_rounds": config.get("time_config", {}).get("total_simulation_hours", 72) * 2,
"total_rounds": config.get("time_config", {}).get(
"total_simulation_hours", 72
)
* 2,
"agents_count": len(config.get("agent_configs", [])),
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
def log_simulation_end(self, platform: str, total_rounds: int, total_actions: int):
entry = {
"timestamp": datetime.now().isoformat(),
@ -283,23 +292,23 @@ class ActionLogger:
"total_rounds": total_rounds,
"total_actions": total_actions,
}
with open(self.log_path, 'a', encoding='utf-8') as f:
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
with open(self.log_path, "a", encoding="utf-8") as f:
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
# 全局日志实例(兼容旧接口)
# Global logger instance (legacy interface compatible)
_global_logger: Optional[ActionLogger] = None
def get_logger(log_path: Optional[str] = None) -> ActionLogger:
"""获取全局日志实例(兼容旧接口)"""
"""Get the global logger instance (legacy interface compatible)"""
global _global_logger
if log_path:
_global_logger = ActionLogger(log_path)
if _global_logger is None:
_global_logger = ActionLogger("actions.jsonl")
return _global_logger

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,8 @@
"""
测试Profile格式生成是否符合OASIS要求
验证
1. Twitter Profile生成CSV格式
2. Reddit Profile生成JSON详细格式
Test Profile format generation against OASIS requirements.
Validation:
1. Twitter Profile generates CSV format
2. Reddit Profile generates detailed JSON format
"""
import os
@ -11,19 +11,22 @@ import json
import csv
import tempfile
# 添加项目路径
# Add project path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from app.services.oasis_profile_generator import OasisProfileGenerator, OasisAgentProfile
from app.services.oasis_profile_generator import (
OasisProfileGenerator,
OasisAgentProfile,
)
def test_profile_formats():
"""测试Profile格式"""
"""Test Profile formats"""
print("=" * 60)
print("OASIS Profile格式测试")
print("OASIS Profile format test")
print("=" * 60)
# 创建测试Profile数据
# Create test Profile data
test_profiles = [
OasisAgentProfile(
user_id=0,
@ -60,87 +63,102 @@ def test_profile_formats():
source_entity_type="University",
),
]
generator = OasisProfileGenerator.__new__(OasisProfileGenerator)
# 使用临时目录
# Use a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
twitter_path = os.path.join(temp_dir, "twitter_profiles.csv")
reddit_path = os.path.join(temp_dir, "reddit_profiles.json")
# 测试Twitter CSV格式
print("\n1. 测试Twitter Profile (CSV格式)")
# Test Twitter CSV format
print("\n1. Test Twitter Profile (CSV format)")
print("-" * 40)
generator._save_twitter_csv(test_profiles, twitter_path)
# 读取并验证CSV
with open(twitter_path, 'r', encoding='utf-8') as f:
# Read and validate CSV
with open(twitter_path, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
rows = list(reader)
print(f" 文件: {twitter_path}")
print(f" 行数: {len(rows)}")
print(f" 表头: {list(rows[0].keys())}")
print(f"\n 示例数据 (第1行):")
print(f" File: {twitter_path}")
print(f" Row count: {len(rows)}")
print(f" Headers: {list(rows[0].keys())}")
print(f"\n Sample data (row 1):")
for key, value in rows[0].items():
print(f" {key}: {value}")
# 验证必需字段
required_twitter_fields = ['user_id', 'user_name', 'name', 'bio',
'friend_count', 'follower_count', 'statuses_count', 'created_at']
# Validate required fields
required_twitter_fields = [
"user_id",
"user_name",
"name",
"bio",
"friend_count",
"follower_count",
"statuses_count",
"created_at",
]
missing = set(required_twitter_fields) - set(rows[0].keys())
if missing:
print(f"\n [错误] 缺少字段: {missing}")
print(f"\n [ERROR] Missing fields: {missing}")
else:
print(f"\n [通过] 所有必需字段都存在")
# 测试Reddit JSON格式
print("\n2. 测试Reddit Profile (JSON详细格式)")
print(f"\n [PASS] All required fields are present")
# Test Reddit JSON format
print("\n2. Test Reddit Profile (detailed JSON format)")
print("-" * 40)
generator._save_reddit_json(test_profiles, reddit_path)
# 读取并验证JSON
with open(reddit_path, 'r', encoding='utf-8') as f:
# Read and validate JSON
with open(reddit_path, "r", encoding="utf-8") as f:
reddit_data = json.load(f)
print(f" 文件: {reddit_path}")
print(f" 条目数: {len(reddit_data)}")
print(f" 字段: {list(reddit_data[0].keys())}")
print(f"\n 示例数据 (第1条):")
print(f" File: {reddit_path}")
print(f" Entry count: {len(reddit_data)}")
print(f" Fields: {list(reddit_data[0].keys())}")
print(f"\n Sample data (entry 1):")
print(json.dumps(reddit_data[0], ensure_ascii=False, indent=4))
# 验证详细格式字段
required_reddit_fields = ['realname', 'username', 'bio', 'persona']
optional_reddit_fields = ['age', 'gender', 'mbti', 'country', 'profession', 'interested_topics']
# Validate detailed format fields
required_reddit_fields = ["realname", "username", "bio", "persona"]
optional_reddit_fields = [
"age",
"gender",
"mbti",
"country",
"profession",
"interested_topics",
]
missing = set(required_reddit_fields) - set(reddit_data[0].keys())
if missing:
print(f"\n [错误] 缺少必需字段: {missing}")
print(f"\n [ERROR] Missing required fields: {missing}")
else:
print(f"\n [通过] 所有必需字段都存在")
print(f"\n [PASS] All required fields are present")
present_optional = set(optional_reddit_fields) & set(reddit_data[0].keys())
print(f" [信息] 可选字段: {present_optional}")
print(f" [INFO] Optional fields: {present_optional}")
print("\n" + "=" * 60)
print("测试完成!")
print("Test completed!")
print("=" * 60)
def show_expected_formats():
"""显示OASIS期望的格式"""
"""Show the formats expected by OASIS"""
print("\n" + "=" * 60)
print("OASIS 期望的Profile格式参考")
print("OASIS expected Profile format reference")
print("=" * 60)
print("\n1. Twitter Profile (CSV格式)")
print("\n1. Twitter Profile (CSV format)")
print("-" * 40)
twitter_example = """user_id,user_name,name,bio,friend_count,follower_count,statuses_count,created_at
0,user0,User Zero,I am user zero with interests in technology.,100,150,500,2023-01-01
1,user1,User One,Tech enthusiast and coffee lover.,200,250,1000,2023-01-02"""
print(twitter_example)
print("\n2. Reddit Profile (JSON详细格式)")
print("\n2. Reddit Profile (detailed JSON format)")
print("-" * 40)
reddit_example = [
{
@ -153,7 +171,7 @@ def show_expected_formats():
"mbti": "ESTJ",
"country": "UK",
"profession": "Hospitality & Tourism",
"interested_topics": ["Economics", "Business"]
"interested_topics": ["Economics", "Business"],
}
]
print(json.dumps(reddit_example, ensure_ascii=False, indent=2))
@ -162,5 +180,3 @@ def show_expected_formats():
if __name__ == "__main__":
test_profile_formats()
show_expected_formats()

View File

@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.11, <3.13"
requires-python = ">=3.11"
resolution-markers = [
"python_full_version >= '3.12'",
"python_full_version < '3.12'",
@ -30,7 +30,7 @@ version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
wheels = [
@ -234,6 +234,40 @@ wheels = [
{ 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]]
@ -292,6 +326,38 @@ wheels = [
{ 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" },
]
@ -340,6 +406,21 @@ wheels = [
{ 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" },
@ -526,6 +607,20 @@ 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" },
@ -727,6 +822,49 @@ wheels = [
{ 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/fe/54/5339ef1ecaa881c6948669956567a64d2670941925f245c434f494ffb0e5/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:4739a4657179ebf08f85914ce50332495811004cc1747852e8b2041ed2aab9b8", size = 311144, upload-time = "2025-11-09T20:49:10.503Z" },
{ url = "https://files.pythonhosted.org/packages/27/74/3446c652bffbd5e81ab354e388b1b5fc1d20daac34ee0ed11ff096b1b01a/jiter-0.12.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:41da8def934bf7bec16cb24bd33c0ca62126d2d45d81d17b864bd5ad721393c3", size = 305877, upload-time = "2025-11-09T20:49:12.269Z" },
{ url = "https://files.pythonhosted.org/packages/a1/f4/ed76ef9043450f57aac2d4fbeb27175aa0eb9c38f833be6ef6379b3b9a86/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c44ee814f499c082e69872d426b624987dbc5943ab06e9bbaa4f81989fdb79e", size = 340419, upload-time = "2025-11-09T20:49:13.803Z" },
@ -853,6 +991,24 @@ wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/41/a0/b91504515c1f9a299fc157967ffbd2f0321bce0516a3d5b89f6f4cad0355/lazy_object_proxy-1.12.0-pp39.pp310.pp311.graalpy311-none-any.whl", hash = "sha256:c3b2e0af1f7f77c4263759c4824316ce458fabe0fceadcd24ef8ca08b2d1e402", size = 15072, upload-time = "2025-08-22T13:50:05.498Z" },
]
@ -896,6 +1052,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c6/80/c06de80bfce881d0ad738576f243911fccf992687ae09fd80b734712b39c/lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849", size = 3611456, upload-time = "2025-09-22T04:01:48.243Z" },
{ url = "https://files.pythonhosted.org/packages/f7/d7/0cdfb6c3e30893463fb3d1e52bc5f5f99684a03c29a0b6b605cfae879cd5/lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f", size = 4011793, upload-time = "2025-09-22T04:01:50.042Z" },
{ url = "https://files.pythonhosted.org/packages/ea/7b/93c73c67db235931527301ed3785f849c78991e2e34f3fd9a6663ffda4c5/lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6", size = 3672836, upload-time = "2025-09-22T04:01:52.145Z" },
{ url = "https://files.pythonhosted.org/packages/53/fd/4e8f0540608977aea078bf6d79f128e0e2c2bba8af1acf775c30baa70460/lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77", size = 8648494, upload-time = "2025-09-22T04:01:54.242Z" },
{ url = "https://files.pythonhosted.org/packages/5d/f4/2a94a3d3dfd6c6b433501b8d470a1960a20ecce93245cf2db1706adf6c19/lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f", size = 4661146, upload-time = "2025-09-22T04:01:56.282Z" },
{ url = "https://files.pythonhosted.org/packages/25/2e/4efa677fa6b322013035d38016f6ae859d06cac67437ca7dc708a6af7028/lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452", size = 4946932, upload-time = "2025-09-22T04:01:58.989Z" },
{ url = "https://files.pythonhosted.org/packages/ce/0f/526e78a6d38d109fdbaa5049c62e1d32fdd70c75fb61c4eadf3045d3d124/lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048", size = 5100060, upload-time = "2025-09-22T04:02:00.812Z" },
{ url = "https://files.pythonhosted.org/packages/81/76/99de58d81fa702cc0ea7edae4f4640416c2062813a00ff24bd70ac1d9c9b/lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df", size = 5019000, upload-time = "2025-09-22T04:02:02.671Z" },
{ url = "https://files.pythonhosted.org/packages/b5/35/9e57d25482bc9a9882cb0037fdb9cc18f4b79d85df94fa9d2a89562f1d25/lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1", size = 5348496, upload-time = "2025-09-22T04:02:04.904Z" },
{ url = "https://files.pythonhosted.org/packages/a6/8e/cb99bd0b83ccc3e8f0f528e9aa1f7a9965dfec08c617070c5db8d63a87ce/lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916", size = 5643779, upload-time = "2025-09-22T04:02:06.689Z" },
{ url = "https://files.pythonhosted.org/packages/d0/34/9e591954939276bb679b73773836c6684c22e56d05980e31d52a9a8deb18/lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd", size = 5244072, upload-time = "2025-09-22T04:02:08.587Z" },
{ url = "https://files.pythonhosted.org/packages/8d/27/b29ff065f9aaca443ee377aff699714fcbffb371b4fce5ac4ca759e436d5/lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6", size = 4718675, upload-time = "2025-09-22T04:02:10.783Z" },
{ url = "https://files.pythonhosted.org/packages/2b/9f/f756f9c2cd27caa1a6ef8c32ae47aadea697f5c2c6d07b0dae133c244fbe/lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a", size = 5255171, upload-time = "2025-09-22T04:02:12.631Z" },
{ url = "https://files.pythonhosted.org/packages/61/46/bb85ea42d2cb1bd8395484fd72f38e3389611aa496ac7772da9205bbda0e/lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679", size = 5057175, upload-time = "2025-09-22T04:02:14.718Z" },
{ url = "https://files.pythonhosted.org/packages/95/0c/443fc476dcc8e41577f0af70458c50fe299a97bb6b7505bb1ae09aa7f9ac/lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659", size = 4785688, upload-time = "2025-09-22T04:02:16.957Z" },
{ url = "https://files.pythonhosted.org/packages/48/78/6ef0b359d45bb9697bc5a626e1992fa5d27aa3f8004b137b2314793b50a0/lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484", size = 5660655, upload-time = "2025-09-22T04:02:18.815Z" },
{ url = "https://files.pythonhosted.org/packages/ff/ea/e1d33808f386bc1339d08c0dcada6e4712d4ed8e93fcad5f057070b7988a/lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2", size = 5247695, upload-time = "2025-09-22T04:02:20.593Z" },
{ url = "https://files.pythonhosted.org/packages/4f/47/eba75dfd8183673725255247a603b4ad606f4ae657b60c6c145b381697da/lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314", size = 5269841, upload-time = "2025-09-22T04:02:22.489Z" },
{ url = "https://files.pythonhosted.org/packages/76/04/5c5e2b8577bc936e219becb2e98cdb1aca14a4921a12995b9d0c523502ae/lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2", size = 3610700, upload-time = "2025-09-22T04:02:24.465Z" },
{ url = "https://files.pythonhosted.org/packages/fe/0a/4643ccc6bb8b143e9f9640aa54e38255f9d3b45feb2cbe7ae2ca47e8782e/lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7", size = 4010347, upload-time = "2025-09-22T04:02:26.286Z" },
{ url = "https://files.pythonhosted.org/packages/31/ef/dcf1d29c3f530577f61e5fe2f1bd72929acf779953668a8a47a479ae6f26/lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf", size = 3671248, upload-time = "2025-09-22T04:02:27.918Z" },
{ url = "https://files.pythonhosted.org/packages/03/15/d4a377b385ab693ce97b472fe0c77c2b16ec79590e688b3ccc71fba19884/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe", size = 8659801, upload-time = "2025-09-22T04:02:30.113Z" },
{ url = "https://files.pythonhosted.org/packages/c8/e8/c128e37589463668794d503afaeb003987373c5f94d667124ffd8078bbd9/lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d", size = 4659403, upload-time = "2025-09-22T04:02:32.119Z" },
{ url = "https://files.pythonhosted.org/packages/00/ce/74903904339decdf7da7847bb5741fc98a5451b42fc419a86c0c13d26fe2/lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d", size = 4966974, upload-time = "2025-09-22T04:02:34.155Z" },
{ url = "https://files.pythonhosted.org/packages/1f/d3/131dec79ce61c5567fecf82515bd9bc36395df42501b50f7f7f3bd065df0/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5", size = 5102953, upload-time = "2025-09-22T04:02:36.054Z" },
{ url = "https://files.pythonhosted.org/packages/3a/ea/a43ba9bb750d4ffdd885f2cd333572f5bb900cd2408b67fdda07e85978a0/lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0", size = 5055054, upload-time = "2025-09-22T04:02:38.154Z" },
{ url = "https://files.pythonhosted.org/packages/60/23/6885b451636ae286c34628f70a7ed1fcc759f8d9ad382d132e1c8d3d9bfd/lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba", size = 5352421, upload-time = "2025-09-22T04:02:40.413Z" },
{ url = "https://files.pythonhosted.org/packages/48/5b/fc2ddfc94ddbe3eebb8e9af6e3fd65e2feba4967f6a4e9683875c394c2d8/lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0", size = 5673684, upload-time = "2025-09-22T04:02:42.288Z" },
{ url = "https://files.pythonhosted.org/packages/29/9c/47293c58cc91769130fbf85531280e8cc7868f7fbb6d92f4670071b9cb3e/lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d", size = 5252463, upload-time = "2025-09-22T04:02:44.165Z" },
{ url = "https://files.pythonhosted.org/packages/9b/da/ba6eceb830c762b48e711ded880d7e3e89fc6c7323e587c36540b6b23c6b/lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37", size = 4698437, upload-time = "2025-09-22T04:02:46.524Z" },
{ url = "https://files.pythonhosted.org/packages/a5/24/7be3f82cb7990b89118d944b619e53c656c97dc89c28cfb143fdb7cd6f4d/lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9", size = 5269890, upload-time = "2025-09-22T04:02:48.812Z" },
{ url = "https://files.pythonhosted.org/packages/1b/bd/dcfb9ea1e16c665efd7538fc5d5c34071276ce9220e234217682e7d2c4a5/lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917", size = 5097185, upload-time = "2025-09-22T04:02:50.746Z" },
{ url = "https://files.pythonhosted.org/packages/21/04/a60b0ff9314736316f28316b694bccbbabe100f8483ad83852d77fc7468e/lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f", size = 4745895, upload-time = "2025-09-22T04:02:52.968Z" },
{ url = "https://files.pythonhosted.org/packages/d6/bd/7d54bd1846e5a310d9c715921c5faa71cf5c0853372adf78aee70c8d7aa2/lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8", size = 5695246, upload-time = "2025-09-22T04:02:54.798Z" },
{ url = "https://files.pythonhosted.org/packages/fd/32/5643d6ab947bc371da21323acb2a6e603cedbe71cb4c99c8254289ab6f4e/lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a", size = 5260797, upload-time = "2025-09-22T04:02:57.058Z" },
{ url = "https://files.pythonhosted.org/packages/33/da/34c1ec4cff1eea7d0b4cd44af8411806ed943141804ac9c5d565302afb78/lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c", size = 5277404, upload-time = "2025-09-22T04:02:58.966Z" },
{ url = "https://files.pythonhosted.org/packages/82/57/4eca3e31e54dc89e2c3507e1cd411074a17565fa5ffc437c4ae0a00d439e/lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b", size = 3670072, upload-time = "2025-09-22T04:03:38.05Z" },
{ url = "https://files.pythonhosted.org/packages/e3/e0/c96cf13eccd20c9421ba910304dae0f619724dcf1702864fd59dd386404d/lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed", size = 4080617, upload-time = "2025-09-22T04:03:39.835Z" },
{ url = "https://files.pythonhosted.org/packages/d5/5d/b3f03e22b3d38d6f188ef044900a9b29b2fe0aebb94625ce9fe244011d34/lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8", size = 3754930, upload-time = "2025-09-22T04:03:41.565Z" },
{ url = "https://files.pythonhosted.org/packages/5e/5c/42c2c4c03554580708fc738d13414801f340c04c3eff90d8d2d227145275/lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d", size = 8910380, upload-time = "2025-09-22T04:03:01.645Z" },
{ url = "https://files.pythonhosted.org/packages/bf/4f/12df843e3e10d18d468a7557058f8d3733e8b6e12401f30b1ef29360740f/lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba", size = 4775632, upload-time = "2025-09-22T04:03:03.814Z" },
{ url = "https://files.pythonhosted.org/packages/e4/0c/9dc31e6c2d0d418483cbcb469d1f5a582a1cd00a1f4081953d44051f3c50/lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601", size = 4975171, upload-time = "2025-09-22T04:03:05.651Z" },
{ url = "https://files.pythonhosted.org/packages/e7/2b/9b870c6ca24c841bdd887504808f0417aa9d8d564114689266f19ddf29c8/lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed", size = 5110109, upload-time = "2025-09-22T04:03:07.452Z" },
{ url = "https://files.pythonhosted.org/packages/bf/0c/4f5f2a4dd319a178912751564471355d9019e220c20d7db3fb8307ed8582/lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37", size = 5041061, upload-time = "2025-09-22T04:03:09.297Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/554eed290365267671fe001a20d72d14f468ae4e6acef1e179b039436967/lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338", size = 5306233, upload-time = "2025-09-22T04:03:11.651Z" },
{ url = "https://files.pythonhosted.org/packages/7a/31/1d748aa275e71802ad9722df32a7a35034246b42c0ecdd8235412c3396ef/lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9", size = 5604739, upload-time = "2025-09-22T04:03:13.592Z" },
{ url = "https://files.pythonhosted.org/packages/8f/41/2c11916bcac09ed561adccacceaedd2bf0e0b25b297ea92aab99fd03d0fa/lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd", size = 5225119, upload-time = "2025-09-22T04:03:15.408Z" },
{ url = "https://files.pythonhosted.org/packages/99/05/4e5c2873d8f17aa018e6afde417c80cc5d0c33be4854cce3ef5670c49367/lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d", size = 4633665, upload-time = "2025-09-22T04:03:17.262Z" },
{ url = "https://files.pythonhosted.org/packages/0f/c9/dcc2da1bebd6275cdc723b515f93edf548b82f36a5458cca3578bc899332/lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9", size = 5234997, upload-time = "2025-09-22T04:03:19.14Z" },
{ url = "https://files.pythonhosted.org/packages/9c/e2/5172e4e7468afca64a37b81dba152fc5d90e30f9c83c7c3213d6a02a5ce4/lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e", size = 5090957, upload-time = "2025-09-22T04:03:21.436Z" },
{ url = "https://files.pythonhosted.org/packages/a5/b3/15461fd3e5cd4ddcb7938b87fc20b14ab113b92312fc97afe65cd7c85de1/lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d", size = 4764372, upload-time = "2025-09-22T04:03:23.27Z" },
{ url = "https://files.pythonhosted.org/packages/05/33/f310b987c8bf9e61c4dd8e8035c416bd3230098f5e3cfa69fc4232de7059/lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec", size = 5634653, upload-time = "2025-09-22T04:03:25.767Z" },
{ url = "https://files.pythonhosted.org/packages/70/ff/51c80e75e0bc9382158133bdcf4e339b5886c6ee2418b5199b3f1a61ed6d/lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272", size = 5233795, upload-time = "2025-09-22T04:03:27.62Z" },
{ url = "https://files.pythonhosted.org/packages/56/4d/4856e897df0d588789dd844dbed9d91782c4ef0b327f96ce53c807e13128/lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f", size = 5257023, upload-time = "2025-09-22T04:03:30.056Z" },
{ url = "https://files.pythonhosted.org/packages/0f/85/86766dfebfa87bea0ab78e9ff7a4b4b45225df4b4d3b8cc3c03c5cd68464/lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312", size = 3911420, upload-time = "2025-09-22T04:03:32.198Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1a/b248b355834c8e32614650b8008c69ffeb0ceb149c793961dd8c0b991bb3/lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca", size = 4406837, upload-time = "2025-09-22T04:03:34.027Z" },
{ url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
{ url = "https://files.pythonhosted.org/packages/0b/11/29d08bc103a62c0eba8016e7ed5aeebbf1e4312e83b0b1648dd203b0e87d/lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700", size = 3949829, upload-time = "2025-09-22T04:04:45.608Z" },
{ url = "https://files.pythonhosted.org/packages/12/b3/52ab9a3b31e5ab8238da241baa19eec44d2ab426532441ee607165aebb52/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee", size = 4226277, upload-time = "2025-09-22T04:04:47.754Z" },
{ url = "https://files.pythonhosted.org/packages/a0/33/1eaf780c1baad88224611df13b1c2a9dfa460b526cacfe769103ff50d845/lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f", size = 4330433, upload-time = "2025-09-22T04:04:49.907Z" },
@ -932,6 +1142,50 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
@ -994,12 +1248,10 @@ dependencies = [
{ name = "charset-normalizer" },
{ name = "flask" },
{ name = "flask-cors" },
{ name = "httpx" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "pymupdf" },
{ name = "python-dotenv" },
{ name = "zep-cloud" },
]
[package.optional-dependencies]
@ -1023,7 +1275,6 @@ requires-dist = [
{ name = "charset-normalizer", specifier = ">=3.0.0" },
{ name = "flask", specifier = ">=3.0.0" },
{ name = "flask-cors", specifier = ">=6.0.0" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "openai", specifier = ">=1.0.0" },
{ name = "pipreqs", marker = "extra == 'dev'", specifier = ">=0.5.0" },
{ name = "pydantic", specifier = ">=2.0.0" },
@ -1031,7 +1282,6 @@ requires-dist = [
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "zep-cloud", specifier = "==3.25.0" },
]
provides-extras = ["dev"]
@ -1146,18 +1396,17 @@ wheels = [
[[package]]
name = "nltk"
version = "3.10.0"
version = "3.9.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "defusedxml" },
{ name = "joblib" },
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/02/df4f105b28a7c16b0e41423bc09cf0f1b8a305df4ef0b10ca74a2e4c648c/nltk-3.10.0.tar.gz", hash = "sha256:4fbac1d98203cbcd1b5d94a2877fb822300072d80604a5e7fae49d2c5f84e8c1", size = 3089244, upload-time = "2026-07-08T02:39:13.562Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629, upload-time = "2025-10-01T07:19:23.764Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/89/a0b0f35e2820d6a99d75ea1c11977ee6d5c9e6658eceb45b0c7620881faa/nltk-3.10.0-py3-none-any.whl", hash = "sha256:54ff84d4916d3ef127e8953bee0023f6a6b320b75d634a19e06ef056d3d244bf", size = 1716144, upload-time = "2026-07-08T02:39:09.753Z" },
{ url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404, upload-time = "2025-10-01T07:19:21.648Z" },
]
[[package]]
@ -1197,6 +1446,50 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" },
{ url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" },
{ url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" },
{ url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" },
{ url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" },
{ url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" },
{ url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" },
{ url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" },
{ url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" },
{ url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" },
{ url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" },
{ url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" },
{ url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" },
{ url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" },
{ url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" },
{ url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" },
{ url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" },
{ url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" },
{ url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" },
{ url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" },
{ url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" },
{ url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" },
{ url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" },
{ url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" },
{ url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" },
{ url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" },
{ url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" },
{ url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" },
{ url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" },
{ url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" },
{ url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" },
{ url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" },
{ url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" },
{ url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" },
{ url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" },
{ url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" },
{ url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" },
{ url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" },
{ url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" },
{ url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" },
{ url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" },
{ url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" },
{ url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" },
{ url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" },
{ url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" },
{ url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" },
{ url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" },
{ url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" },
@ -1682,6 +1975,48 @@ wheels = [
{ 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/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
@ -1856,6 +2191,12 @@ 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]]
@ -1883,6 +2224,34 @@ wheels = [
{ 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]]
@ -1914,6 +2283,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" },
{ url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" },
{ url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" },
{ url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" },
{ url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" },
{ url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" },
{ url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" },
{ url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" },
{ url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" },
{ url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" },
{ url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" },
{ url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" },
{ url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" },
{ url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" },
{ url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" },
{ url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" },
{ url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" },
{ url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" },
{ url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" },
{ url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" },
{ url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" },
{ url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" },
{ url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" },
{ url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" },
{ url = "https://files.pythonhosted.org/packages/4c/c6/c4dcdecdbaa70969ee1fdced6d7b8f60cfabe64d25361f27ac4665a70620/pyzmq-27.1.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:18770c8d3563715387139060d37859c02ce40718d1faf299abddcdcc6a649066", size = 836265, upload-time = "2025-09-08T23:09:49.376Z" },
{ url = "https://files.pythonhosted.org/packages/3e/79/f38c92eeaeb03a2ccc2ba9866f0439593bb08c5e3b714ac1d553e5c96e25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:ac25465d42f92e990f8d8b0546b01c391ad431c3bf447683fdc40565941d0604", size = 800208, upload-time = "2025-09-08T23:09:51.073Z" },
{ url = "https://files.pythonhosted.org/packages/49/0e/3f0d0d335c6b3abb9b7b723776d0b21fa7f3a6c819a0db6097059aada160/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53b40f8ae006f2734ee7608d59ed661419f087521edbfc2149c3932e9c14808c", size = 567747, upload-time = "2025-09-08T23:09:52.698Z" },
@ -1949,6 +2340,50 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" },
{ url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" },
{ url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" },
{ url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" },
{ url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" },
{ url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" },
{ url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" },
{ url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" },
{ url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" },
{ url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" },
{ url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" },
{ url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" },
{ url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" },
{ url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" },
{ url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" },
{ url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" },
{ url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" },
{ url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" },
{ url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" },
{ url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" },
{ url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" },
{ url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" },
{ url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" },
{ url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" },
{ url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" },
{ url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" },
{ url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" },
{ url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" },
{ url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" },
{ url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" },
{ url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" },
{ url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" },
{ url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" },
{ url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" },
{ url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" },
{ url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" },
{ url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" },
{ url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" },
{ url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" },
{ url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" },
{ url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" },
{ url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" },
{ url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" },
{ url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" },
{ url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" },
{ url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" },
{ url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" },
{ url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" },
@ -1963,7 +2398,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions" },
{ 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 = [
@ -2004,6 +2439,62 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" },
{ url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" },
{ url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" },
{ url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081, upload-time = "2025-11-03T21:31:55.9Z" },
{ url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123, upload-time = "2025-11-03T21:31:57.758Z" },
{ url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814, upload-time = "2025-11-03T21:32:01.12Z" },
{ url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592, upload-time = "2025-11-03T21:32:03.006Z" },
{ url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122, upload-time = "2025-11-03T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272, upload-time = "2025-11-03T21:32:06.148Z" },
{ url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497, upload-time = "2025-11-03T21:32:08.162Z" },
{ url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892, upload-time = "2025-11-03T21:32:09.769Z" },
{ url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462, upload-time = "2025-11-03T21:32:11.769Z" },
{ url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528, upload-time = "2025-11-03T21:32:13.906Z" },
{ url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866, upload-time = "2025-11-03T21:32:15.748Z" },
{ url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189, upload-time = "2025-11-03T21:32:17.493Z" },
{ url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054, upload-time = "2025-11-03T21:32:19.042Z" },
{ url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325, upload-time = "2025-11-03T21:32:21.338Z" },
{ url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984, upload-time = "2025-11-03T21:32:23.466Z" },
{ url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673, upload-time = "2025-11-03T21:32:25.034Z" },
{ url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029, upload-time = "2025-11-03T21:32:26.528Z" },
{ url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437, upload-time = "2025-11-03T21:32:28.363Z" },
{ url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368, upload-time = "2025-11-03T21:32:30.4Z" },
{ url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921, upload-time = "2025-11-03T21:32:32.123Z" },
{ url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708, upload-time = "2025-11-03T21:32:34.305Z" },
{ url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472, upload-time = "2025-11-03T21:32:36.364Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341, upload-time = "2025-11-03T21:32:38.042Z" },
{ url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666, upload-time = "2025-11-03T21:32:40.079Z" },
{ url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473, upload-time = "2025-11-03T21:32:42.148Z" },
{ url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792, upload-time = "2025-11-03T21:32:44.13Z" },
{ url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214, upload-time = "2025-11-03T21:32:45.853Z" },
{ url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469, upload-time = "2025-11-03T21:32:48.026Z" },
{ url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089, upload-time = "2025-11-03T21:32:50.027Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059, upload-time = "2025-11-03T21:32:51.682Z" },
{ url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900, upload-time = "2025-11-03T21:32:53.569Z" },
{ url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010, upload-time = "2025-11-03T21:32:55.222Z" },
{ url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893, upload-time = "2025-11-03T21:32:57.239Z" },
{ url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522, upload-time = "2025-11-03T21:32:59.274Z" },
{ url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272, upload-time = "2025-11-03T21:33:01.393Z" },
{ url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958, upload-time = "2025-11-03T21:33:03.379Z" },
{ url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289, upload-time = "2025-11-03T21:33:05.374Z" },
{ url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026, upload-time = "2025-11-03T21:33:07.131Z" },
{ url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499, upload-time = "2025-11-03T21:33:09.141Z" },
{ url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604, upload-time = "2025-11-03T21:33:10.9Z" },
{ url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320, upload-time = "2025-11-03T21:33:12.572Z" },
{ url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372, upload-time = "2025-11-03T21:33:14.219Z" },
{ url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985, upload-time = "2025-11-03T21:33:16.555Z" },
{ url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669, upload-time = "2025-11-03T21:33:18.32Z" },
{ url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030, upload-time = "2025-11-03T21:33:20.048Z" },
{ url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674, upload-time = "2025-11-03T21:33:21.797Z" },
{ url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451, upload-time = "2025-11-03T21:33:23.741Z" },
{ url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980, upload-time = "2025-11-03T21:33:25.999Z" },
{ url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852, upload-time = "2025-11-03T21:33:27.852Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566, upload-time = "2025-11-03T21:33:32.364Z" },
{ url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463, upload-time = "2025-11-03T21:33:34.459Z" },
{ url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694, upload-time = "2025-11-03T21:33:36.793Z" },
{ url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691, upload-time = "2025-11-03T21:33:39.079Z" },
{ url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583, upload-time = "2025-11-03T21:33:41.302Z" },
{ url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286, upload-time = "2025-11-03T21:33:43.324Z" },
{ url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741, upload-time = "2025-11-03T21:33:45.557Z" },
]
[[package]]
@ -2094,6 +2585,64 @@ wheels = [
{ 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" },
{ url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
{ url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
{ url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
@ -2113,7 +2662,7 @@ name = "ruamel-yaml"
version = "0.18.17"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" },
{ name = "ruamel-yaml-clib", marker = "python_full_version < '3.15' and platform_python_implementation == 'CPython'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/2b/7a1f1ebcd6b3f14febdc003e658778d81e76b40df2267904ee6b13f0c5c6/ruamel_yaml-0.18.17.tar.gz", hash = "sha256:9091cd6e2d93a3a4b157ddb8fabf348c3de7f1fb1381346d985b6b247dcd8d3c", size = 149602, upload-time = "2025-12-17T20:02:55.757Z" }
wheels = [
@ -2146,6 +2695,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/21/ee353e882350beab65fcc47a91b6bdc512cace4358ee327af2962892ff16/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5e9f630c73a490b758bf14d859a39f375e6999aea5ddd2e2e9da89b9953486a", size = 771624, upload-time = "2025-11-16T16:13:29.853Z" },
{ url = "https://files.pythonhosted.org/packages/57/34/cc1b94057aa867c963ecf9ea92ac59198ec2ee3a8d22a126af0b4d4be712/ruamel_yaml_clib-0.2.15-cp312-cp312-win32.whl", hash = "sha256:f4421ab780c37210a07d138e56dd4b51f8642187cdfb433eb687fe8c11de0144", size = 100342, upload-time = "2025-11-16T16:13:31.067Z" },
{ url = "https://files.pythonhosted.org/packages/b3/e5/8925a4208f131b218f9a7e459c0d6fcac8324ae35da269cb437894576366/ruamel_yaml_clib-0.2.15-cp312-cp312-win_amd64.whl", hash = "sha256:2b216904750889133d9222b7b873c199d48ecbb12912aca78970f84a5aa1a4bc", size = 119013, upload-time = "2025-11-16T16:13:32.164Z" },
{ url = "https://files.pythonhosted.org/packages/17/5e/2f970ce4c573dc30c2f95825f2691c96d55560268ddc67603dc6ea2dd08e/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dcec721fddbb62e60c2801ba08c87010bd6b700054a09998c4d09c08147b8fb", size = 147450, upload-time = "2025-11-16T16:13:33.542Z" },
{ url = "https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471", size = 133139, upload-time = "2025-11-16T16:13:34.587Z" },
{ url = "https://files.pythonhosted.org/packages/dc/19/40d676802390f85784235a05788fd28940923382e3f8b943d25febbb98b7/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:46895c17ead5e22bea5e576f1db7e41cb273e8d062c04a6a49013d9f60996c25", size = 731474, upload-time = "2025-11-16T20:22:49.934Z" },
{ url = "https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a", size = 748047, upload-time = "2025-11-16T16:13:35.633Z" },
{ url = "https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf", size = 782129, upload-time = "2025-11-16T16:13:36.781Z" },
{ url = "https://files.pythonhosted.org/packages/de/4b/e98086e88f76c00c88a6bcf15eae27a1454f661a9eb72b111e6bbb69024d/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab0df0648d86a7ecbd9c632e8f8d6b21bb21b5fc9d9e095c796cacf32a728d2d", size = 736848, upload-time = "2025-11-16T16:13:37.952Z" },
{ url = "https://files.pythonhosted.org/packages/0c/5c/5964fcd1fd9acc53b7a3a5d9a05ea4f95ead9495d980003a557deb9769c7/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:331fb180858dd8534f0e61aa243b944f25e73a4dae9962bd44c46d1761126bbf", size = 741630, upload-time = "2025-11-16T20:22:51.718Z" },
{ url = "https://files.pythonhosted.org/packages/07/1e/99660f5a30fceb58494598e7d15df883a07292346ef5696f0c0ae5dee8c6/ruamel_yaml_clib-0.2.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51", size = 766619, upload-time = "2025-11-16T16:13:39.178Z" },
{ url = "https://files.pythonhosted.org/packages/36/2f/fa0344a9327b58b54970e56a27b32416ffbcfe4dcc0700605516708579b2/ruamel_yaml_clib-0.2.15-cp313-cp313-win32.whl", hash = "sha256:bf0846d629e160223805db9fe8cc7aec16aaa11a07310c50c8c7164efa440aec", size = 100171, upload-time = "2025-11-16T16:13:40.456Z" },
{ url = "https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl", hash = "sha256:45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6", size = 118845, upload-time = "2025-11-16T16:13:41.481Z" },
{ url = "https://files.pythonhosted.org/packages/3e/bd/ab8459c8bb759c14a146990bf07f632c1cbec0910d4853feeee4be2ab8bb/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:753faf20b3a5906faf1fc50e4ddb8c074cb9b251e00b14c18b28492f933ac8ef", size = 147248, upload-time = "2025-11-16T16:13:42.872Z" },
{ url = "https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:480894aee0b29752560a9de46c0e5f84a82602f2bc5c6cde8db9a345319acfdf", size = 133764, upload-time = "2025-11-16T16:13:43.932Z" },
{ url = "https://files.pythonhosted.org/packages/82/c7/2480d062281385a2ea4f7cc9476712446e0c548cd74090bff92b4b49e898/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d3b58ab2454b4747442ac76fab66739c72b1e2bb9bd173d7694b9f9dbc9c000", size = 730537, upload-time = "2025-11-16T20:22:52.918Z" },
{ url = "https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bfd309b316228acecfa30670c3887dcedf9b7a44ea39e2101e75d2654522acd4", size = 746944, upload-time = "2025-11-16T16:13:45.338Z" },
{ url = "https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2812ff359ec1f30129b62372e5f22a52936fac13d5d21e70373dbca5d64bb97c", size = 778249, upload-time = "2025-11-16T16:13:46.871Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1d/70dbda370bd0e1a92942754c873bd28f513da6198127d1736fa98bb2a16f/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7e74ea87307303ba91073b63e67f2c667e93f05a8c63079ee5b7a5c8d0d7b043", size = 737140, upload-time = "2025-11-16T16:13:48.349Z" },
{ url = "https://files.pythonhosted.org/packages/5b/87/822d95874216922e1120afb9d3fafa795a18fdd0c444f5c4c382f6dac761/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:713cd68af9dfbe0bb588e144a61aad8dcc00ef92a82d2e87183ca662d242f524", size = 741070, upload-time = "2025-11-16T20:22:54.151Z" },
{ url = "https://files.pythonhosted.org/packages/b9/17/4e01a602693b572149f92c983c1f25bd608df02c3f5cf50fd1f94e124a59/ruamel_yaml_clib-0.2.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:542d77b72786a35563f97069b9379ce762944e67055bea293480f7734b2c7e5e", size = 765882, upload-time = "2025-11-16T16:13:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/9f/17/7999399081d39ebb79e807314de6b611e1d1374458924eb2a489c01fc5ad/ruamel_yaml_clib-0.2.15-cp314-cp314-win32.whl", hash = "sha256:424ead8cef3939d690c4b5c85ef5b52155a231ff8b252961b6516ed7cf05f6aa", size = 102567, upload-time = "2025-11-16T16:13:50.78Z" },
{ url = "https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl", hash = "sha256:ac9b8d5fa4bb7fd2917ab5027f60d4234345fd366fe39aa711d5dca090aa1467", size = 122847, upload-time = "2025-11-16T16:13:51.807Z" },
]
[[package]]
@ -2194,6 +2763,30 @@ wheels = [
{ 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]]
@ -2225,6 +2818,46 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/82/31/006cbb4b648ba379a95c87262c2855cd0d09453e500937f78b30f02fa1cd/scipy-1.16.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c5192722cffe15f9329a3948c4b1db789fbb1f05c97899187dcf009b283aea70", size = 38678975, upload-time = "2025-10-28T17:33:15.809Z" },
{ url = "https://files.pythonhosted.org/packages/c2/7f/acbd28c97e990b421af7d6d6cd416358c9c293fc958b8529e0bd5d2a2a19/scipy-1.16.3-cp312-cp312-win_amd64.whl", hash = "sha256:56edc65510d1331dae01ef9b658d428e33ed48b4f77b1d51caf479a0253f96dc", size = 38555926, upload-time = "2025-10-28T17:33:21.388Z" },
{ url = "https://files.pythonhosted.org/packages/ce/69/c5c7807fd007dad4f48e0a5f2153038dc96e8725d3345b9ee31b2b7bed46/scipy-1.16.3-cp312-cp312-win_arm64.whl", hash = "sha256:a8a26c78ef223d3e30920ef759e25625a0ecdd0d60e5a8818b7513c3e5384cf2", size = 25463014, upload-time = "2025-10-28T17:33:25.975Z" },
{ url = "https://files.pythonhosted.org/packages/72/f1/57e8327ab1508272029e27eeef34f2302ffc156b69e7e233e906c2a5c379/scipy-1.16.3-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:d2ec56337675e61b312179a1ad124f5f570c00f920cc75e1000025451b88241c", size = 36617856, upload-time = "2025-10-28T17:33:31.375Z" },
{ url = "https://files.pythonhosted.org/packages/44/13/7e63cfba8a7452eb756306aa2fd9b37a29a323b672b964b4fdeded9a3f21/scipy-1.16.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:16b8bc35a4cc24db80a0ec836a9286d0e31b2503cb2fd7ff7fb0e0374a97081d", size = 28874306, upload-time = "2025-10-28T17:33:36.516Z" },
{ url = "https://files.pythonhosted.org/packages/15/65/3a9400efd0228a176e6ec3454b1fa998fbbb5a8defa1672c3f65706987db/scipy-1.16.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:5803c5fadd29de0cf27fa08ccbfe7a9e5d741bf63e4ab1085437266f12460ff9", size = 20865371, upload-time = "2025-10-28T17:33:42.094Z" },
{ url = "https://files.pythonhosted.org/packages/33/d7/eda09adf009a9fb81827194d4dd02d2e4bc752cef16737cc4ef065234031/scipy-1.16.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:b81c27fc41954319a943d43b20e07c40bdcd3ff7cf013f4fb86286faefe546c4", size = 23524877, upload-time = "2025-10-28T17:33:48.483Z" },
{ url = "https://files.pythonhosted.org/packages/7d/6b/3f911e1ebc364cb81320223a3422aab7d26c9c7973109a9cd0f27c64c6c0/scipy-1.16.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0c3b4dd3d9b08dbce0f3440032c52e9e2ab9f96ade2d3943313dfe51a7056959", size = 33342103, upload-time = "2025-10-28T17:33:56.495Z" },
{ url = "https://files.pythonhosted.org/packages/21/f6/4bfb5695d8941e5c570a04d9fcd0d36bce7511b7d78e6e75c8f9791f82d0/scipy-1.16.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7dc1360c06535ea6116a2220f760ae572db9f661aba2d88074fe30ec2aa1ff88", size = 35697297, upload-time = "2025-10-28T17:34:04.722Z" },
{ url = "https://files.pythonhosted.org/packages/04/e1/6496dadbc80d8d896ff72511ecfe2316b50313bfc3ebf07a3f580f08bd8c/scipy-1.16.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:663b8d66a8748051c3ee9c96465fb417509315b99c71550fda2591d7dd634234", size = 36021756, upload-time = "2025-10-28T17:34:13.482Z" },
{ url = "https://files.pythonhosted.org/packages/fe/bd/a8c7799e0136b987bda3e1b23d155bcb31aec68a4a472554df5f0937eef7/scipy-1.16.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eab43fae33a0c39006a88096cd7b4f4ef545ea0447d250d5ac18202d40b6611d", size = 38696566, upload-time = "2025-10-28T17:34:22.384Z" },
{ url = "https://files.pythonhosted.org/packages/cd/01/1204382461fcbfeb05b6161b594f4007e78b6eba9b375382f79153172b4d/scipy-1.16.3-cp313-cp313-win_amd64.whl", hash = "sha256:062246acacbe9f8210de8e751b16fc37458213f124bef161a5a02c7a39284304", size = 38529877, upload-time = "2025-10-28T17:35:51.076Z" },
{ url = "https://files.pythonhosted.org/packages/7f/14/9d9fbcaa1260a94f4bb5b64ba9213ceb5d03cd88841fe9fd1ffd47a45b73/scipy-1.16.3-cp313-cp313-win_arm64.whl", hash = "sha256:50a3dbf286dbc7d84f176f9a1574c705f277cb6565069f88f60db9eafdbe3ee2", size = 25455366, upload-time = "2025-10-28T17:35:59.014Z" },
{ url = "https://files.pythonhosted.org/packages/e2/a3/9ec205bd49f42d45d77f1730dbad9ccf146244c1647605cf834b3a8c4f36/scipy-1.16.3-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:fb4b29f4cf8cc5a8d628bc8d8e26d12d7278cd1f219f22698a378c3d67db5e4b", size = 37027931, upload-time = "2025-10-28T17:34:31.451Z" },
{ url = "https://files.pythonhosted.org/packages/25/06/ca9fd1f3a4589cbd825b1447e5db3a8ebb969c1eaf22c8579bd286f51b6d/scipy-1.16.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:8d09d72dc92742988b0e7750bddb8060b0c7079606c0d24a8cc8e9c9c11f9079", size = 29400081, upload-time = "2025-10-28T17:34:39.087Z" },
{ url = "https://files.pythonhosted.org/packages/6a/56/933e68210d92657d93fb0e381683bc0e53a965048d7358ff5fbf9e6a1b17/scipy-1.16.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:03192a35e661470197556de24e7cb1330d84b35b94ead65c46ad6f16f6b28f2a", size = 21391244, upload-time = "2025-10-28T17:34:45.234Z" },
{ url = "https://files.pythonhosted.org/packages/a8/7e/779845db03dc1418e215726329674b40576879b91814568757ff0014ad65/scipy-1.16.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:57d01cb6f85e34f0946b33caa66e892aae072b64b034183f3d87c4025802a119", size = 23929753, upload-time = "2025-10-28T17:34:51.793Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4b/f756cf8161d5365dcdef9e5f460ab226c068211030a175d2fc7f3f41ca64/scipy-1.16.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:96491a6a54e995f00a28a3c3badfff58fd093bf26cd5fb34a2188c8c756a3a2c", size = 33496912, upload-time = "2025-10-28T17:34:59.8Z" },
{ url = "https://files.pythonhosted.org/packages/09/b5/222b1e49a58668f23839ca1542a6322bb095ab8d6590d4f71723869a6c2c/scipy-1.16.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd13e354df9938598af2be05822c323e97132d5e6306b83a3b4ee6724c6e522e", size = 35802371, upload-time = "2025-10-28T17:35:08.173Z" },
{ url = "https://files.pythonhosted.org/packages/c1/8d/5964ef68bb31829bde27611f8c9deeac13764589fe74a75390242b64ca44/scipy-1.16.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63d3cdacb8a824a295191a723ee5e4ea7768ca5ca5f2838532d9f2e2b3ce2135", size = 36190477, upload-time = "2025-10-28T17:35:16.7Z" },
{ url = "https://files.pythonhosted.org/packages/ab/f2/b31d75cb9b5fa4dd39a0a931ee9b33e7f6f36f23be5ef560bf72e0f92f32/scipy-1.16.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e7efa2681ea410b10dde31a52b18b0154d66f2485328830e45fdf183af5aefc6", size = 38796678, upload-time = "2025-10-28T17:35:26.354Z" },
{ url = "https://files.pythonhosted.org/packages/b4/1e/b3723d8ff64ab548c38d87055483714fefe6ee20e0189b62352b5e015bb1/scipy-1.16.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2d1ae2cf0c350e7705168ff2429962a89ad90c2d49d1dd300686d8b2a5af22fc", size = 38640178, upload-time = "2025-10-28T17:35:35.304Z" },
{ url = "https://files.pythonhosted.org/packages/8e/f3/d854ff38789aca9b0cc23008d607ced9de4f7ab14fa1ca4329f86b3758ca/scipy-1.16.3-cp313-cp313t-win_arm64.whl", hash = "sha256:0c623a54f7b79dd88ef56da19bc2873afec9673a48f3b85b18e4d402bdd29a5a", size = 25803246, upload-time = "2025-10-28T17:35:42.155Z" },
{ url = "https://files.pythonhosted.org/packages/99/f6/99b10fd70f2d864c1e29a28bbcaa0c6340f9d8518396542d9ea3b4aaae15/scipy-1.16.3-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:875555ce62743e1d54f06cdf22c1e0bc47b91130ac40fe5d783b6dfa114beeb6", size = 36606469, upload-time = "2025-10-28T17:36:08.741Z" },
{ url = "https://files.pythonhosted.org/packages/4d/74/043b54f2319f48ea940dd025779fa28ee360e6b95acb7cd188fad4391c6b/scipy-1.16.3-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:bb61878c18a470021fb515a843dc7a76961a8daceaaaa8bad1332f1bf4b54657", size = 28872043, upload-time = "2025-10-28T17:36:16.599Z" },
{ url = "https://files.pythonhosted.org/packages/4d/e1/24b7e50cc1c4ee6ffbcb1f27fe9f4c8b40e7911675f6d2d20955f41c6348/scipy-1.16.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2622206f5559784fa5c4b53a950c3c7c1cf3e84ca1b9c4b6c03f062f289ca26", size = 20862952, upload-time = "2025-10-28T17:36:22.966Z" },
{ url = "https://files.pythonhosted.org/packages/dd/3a/3e8c01a4d742b730df368e063787c6808597ccb38636ed821d10b39ca51b/scipy-1.16.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7f68154688c515cdb541a31ef8eb66d8cd1050605be9dcd74199cbd22ac739bc", size = 23508512, upload-time = "2025-10-28T17:36:29.731Z" },
{ url = "https://files.pythonhosted.org/packages/1f/60/c45a12b98ad591536bfe5330cb3cfe1850d7570259303563b1721564d458/scipy-1.16.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3c820ddb80029fe9f43d61b81d8b488d3ef8ca010d15122b152db77dc94c22", size = 33413639, upload-time = "2025-10-28T17:36:37.982Z" },
{ url = "https://files.pythonhosted.org/packages/71/bc/35957d88645476307e4839712642896689df442f3e53b0fa016ecf8a3357/scipy-1.16.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3837938ae715fc0fe3c39c0202de3a8853aff22ca66781ddc2ade7554b7e2cc", size = 35704729, upload-time = "2025-10-28T17:36:46.547Z" },
{ url = "https://files.pythonhosted.org/packages/3b/15/89105e659041b1ca11c386e9995aefacd513a78493656e57789f9d9eab61/scipy-1.16.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aadd23f98f9cb069b3bd64ddc900c4d277778242e961751f77a8cb5c4b946fb0", size = 36086251, upload-time = "2025-10-28T17:36:55.161Z" },
{ url = "https://files.pythonhosted.org/packages/1a/87/c0ea673ac9c6cc50b3da2196d860273bc7389aa69b64efa8493bdd25b093/scipy-1.16.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b7c5f1bda1354d6a19bc6af73a649f8285ca63ac6b52e64e658a5a11d4d69800", size = 38716681, upload-time = "2025-10-28T17:37:04.1Z" },
{ url = "https://files.pythonhosted.org/packages/91/06/837893227b043fb9b0d13e4bd7586982d8136cb249ffb3492930dab905b8/scipy-1.16.3-cp314-cp314-win_amd64.whl", hash = "sha256:e5d42a9472e7579e473879a1990327830493a7047506d58d73fc429b84c1d49d", size = 39358423, upload-time = "2025-10-28T17:38:20.005Z" },
{ url = "https://files.pythonhosted.org/packages/95/03/28bce0355e4d34a7c034727505a02d19548549e190bedd13a721e35380b7/scipy-1.16.3-cp314-cp314-win_arm64.whl", hash = "sha256:6020470b9d00245926f2d5bb93b119ca0340f0d564eb6fbaad843eaebf9d690f", size = 26135027, upload-time = "2025-10-28T17:38:24.966Z" },
{ url = "https://files.pythonhosted.org/packages/b2/6f/69f1e2b682efe9de8fe9f91040f0cd32f13cfccba690512ba4c582b0bc29/scipy-1.16.3-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:e1d27cbcb4602680a49d787d90664fa4974063ac9d4134813332a8c53dbe667c", size = 37028379, upload-time = "2025-10-28T17:37:14.061Z" },
{ url = "https://files.pythonhosted.org/packages/7c/2d/e826f31624a5ebbab1cd93d30fd74349914753076ed0593e1d56a98c4fb4/scipy-1.16.3-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9b9c9c07b6d56a35777a1b4cc8966118fb16cfd8daf6743867d17d36cfad2d40", size = 29400052, upload-time = "2025-10-28T17:37:21.709Z" },
{ url = "https://files.pythonhosted.org/packages/69/27/d24feb80155f41fd1f156bf144e7e049b4e2b9dd06261a242905e3bc7a03/scipy-1.16.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:3a4c460301fb2cffb7f88528f30b3127742cff583603aa7dc964a52c463b385d", size = 21391183, upload-time = "2025-10-28T17:37:29.559Z" },
{ url = "https://files.pythonhosted.org/packages/f8/d3/1b229e433074c5738a24277eca520a2319aac7465eea7310ea6ae0e98ae2/scipy-1.16.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:f667a4542cc8917af1db06366d3f78a5c8e83badd56409f94d1eac8d8d9133fa", size = 23930174, upload-time = "2025-10-28T17:37:36.306Z" },
{ url = "https://files.pythonhosted.org/packages/16/9d/d9e148b0ec680c0f042581a2be79a28a7ab66c0c4946697f9e7553ead337/scipy-1.16.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f379b54b77a597aa7ee5e697df0d66903e41b9c85a6dd7946159e356319158e8", size = 33497852, upload-time = "2025-10-28T17:37:42.228Z" },
{ url = "https://files.pythonhosted.org/packages/2f/22/4e5f7561e4f98b7bea63cf3fd7934bff1e3182e9f1626b089a679914d5c8/scipy-1.16.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4aff59800a3b7f786b70bfd6ab551001cb553244988d7d6b8299cb1ea653b353", size = 35798595, upload-time = "2025-10-28T17:37:48.102Z" },
{ url = "https://files.pythonhosted.org/packages/83/42/6644d714c179429fc7196857866f219fef25238319b650bb32dde7bf7a48/scipy-1.16.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:da7763f55885045036fabcebd80144b757d3db06ab0861415d1c3b7c69042146", size = 36186269, upload-time = "2025-10-28T17:37:53.72Z" },
{ url = "https://files.pythonhosted.org/packages/ac/70/64b4d7ca92f9cf2e6fc6aaa2eecf80bb9b6b985043a9583f32f8177ea122/scipy-1.16.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ffa6eea95283b2b8079b821dc11f50a17d0571c92b43e2b5b12764dc5f9b285d", size = 38802779, upload-time = "2025-10-28T17:37:59.393Z" },
{ url = "https://files.pythonhosted.org/packages/61/82/8d0e39f62764cce5ffd5284131e109f07cf8955aef9ab8ed4e3aa5e30539/scipy-1.16.3-cp314-cp314t-win_amd64.whl", hash = "sha256:d9f48cafc7ce94cf9b15c6bffdc443a81a27bf7075cf2dcd5c8b40f85d10c4e7", size = 39471128, upload-time = "2025-10-28T17:38:05.259Z" },
{ url = "https://files.pythonhosted.org/packages/64/47/a494741db7280eae6dc033510c319e34d42dd41b7ac0c7ead39354d1a2b5/scipy-1.16.3-cp314-cp314t-win_arm64.whl", hash = "sha256:21d9d6b197227a12dcbf9633320a4e34c6b0e51c57268df255a0942983bac562", size = 26464127, upload-time = "2025-10-28T17:38:11.34Z" },
]
[[package]]
@ -2324,7 +2957,7 @@ version = "0.50.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions" },
{ 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 = [
@ -2471,6 +3104,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/17/e377a460603132b00760511299fceba4102bd95db1a0ee788da21298ccff/torch-2.9.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:27331cd902fb4322252657f3902adf1c4f6acad9dcad81d8df3ae14c7c4f07c4", size = 899742281, upload-time = "2025-11-12T15:22:17.602Z" },
{ url = "https://files.pythonhosted.org/packages/b1/1a/64f5769025db846a82567fa5b7d21dba4558a7234ee631712ee4771c436c/torch-2.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:81a285002d7b8cfd3fdf1b98aa8df138d41f1a8334fd9ea37511517cedf43083", size = 110940568, upload-time = "2025-11-12T15:21:18.689Z" },
{ url = "https://files.pythonhosted.org/packages/6e/ab/07739fd776618e5882661d04c43f5b5586323e2f6a2d7d84aac20d8f20bd/torch-2.9.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:c0d25d1d8e531b8343bea0ed811d5d528958f1dcbd37e7245bc686273177ad7e", size = 74479191, upload-time = "2025-11-12T15:21:25.816Z" },
{ url = "https://files.pythonhosted.org/packages/20/60/8fc5e828d050bddfab469b3fe78e5ab9a7e53dda9c3bdc6a43d17ce99e63/torch-2.9.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c29455d2b910b98738131990394da3e50eea8291dfeb4b12de71ecf1fdeb21cb", size = 104135743, upload-time = "2025-11-12T15:21:34.936Z" },
{ url = "https://files.pythonhosted.org/packages/f2/b7/6d3f80e6918213babddb2a37b46dbb14c15b14c5f473e347869a51f40e1f/torch-2.9.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:524de44cd13931208ba2c4bde9ec7741fd4ae6bfd06409a604fc32f6520c2bc9", size = 899749493, upload-time = "2025-11-12T15:24:36.356Z" },
{ url = "https://files.pythonhosted.org/packages/a6/47/c7843d69d6de8938c1cbb1eba426b1d48ddf375f101473d3e31a5fc52b74/torch-2.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:545844cc16b3f91e08ce3b40e9c2d77012dd33a48d505aed34b7740ed627a1b2", size = 110944162, upload-time = "2025-11-12T15:21:53.151Z" },
{ url = "https://files.pythonhosted.org/packages/28/0e/2a37247957e72c12151b33a01e4df651d9d155dd74d8cfcbfad15a79b44a/torch-2.9.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5be4bf7496f1e3ffb1dd44b672adb1ac3f081f204c5ca81eba6442f5f634df8e", size = 74830751, upload-time = "2025-11-12T15:21:43.792Z" },
{ url = "https://files.pythonhosted.org/packages/4b/f7/7a18745edcd7b9ca2381aa03353647bca8aace91683c4975f19ac233809d/torch-2.9.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:30a3e170a84894f3652434b56d59a64a2c11366b0ed5776fab33c2439396bf9a", size = 104142929, upload-time = "2025-11-12T15:21:48.319Z" },
{ url = "https://files.pythonhosted.org/packages/f4/dd/f1c0d879f2863ef209e18823a988dc7a1bf40470750e3ebe927efdb9407f/torch-2.9.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:8301a7b431e51764629208d0edaa4f9e4c33e6df0f2f90b90e261d623df6a4e2", size = 899748978, upload-time = "2025-11-12T15:23:04.568Z" },
{ url = "https://files.pythonhosted.org/packages/1f/9f/6986b83a53b4d043e36f3f898b798ab51f7f20fdf1a9b01a2720f445043d/torch-2.9.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2e1c42c0ae92bf803a4b2409fdfed85e30f9027a66887f5e7dcdbc014c7531db", size = 111176995, upload-time = "2025-11-12T15:22:01.618Z" },
{ url = "https://files.pythonhosted.org/packages/40/60/71c698b466dd01e65d0e9514b5405faae200c52a76901baf6906856f17e4/torch-2.9.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:2c14b3da5df416cf9cb5efab83aa3056f5b8cd8620b8fde81b4987ecab730587", size = 74480347, upload-time = "2025-11-12T15:21:57.648Z" },
{ url = "https://files.pythonhosted.org/packages/48/50/c4b5112546d0d13cc9eaa1c732b823d676a9f49ae8b6f97772f795874a03/torch-2.9.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1edee27a7c9897f4e0b7c14cfc2f3008c571921134522d5b9b5ec4ebbc69041a", size = 74433245, upload-time = "2025-11-12T15:22:39.027Z" },
{ url = "https://files.pythonhosted.org/packages/81/c9/2628f408f0518b3bae49c95f5af3728b6ab498c8624ab1e03a43dd53d650/torch-2.9.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:19d144d6b3e29921f1fc70503e9f2fc572cde6a5115c0c0de2f7ca8b1483e8b6", size = 104134804, upload-time = "2025-11-12T15:22:35.222Z" },
{ url = "https://files.pythonhosted.org/packages/28/fc/5bc91d6d831ae41bf6e9e6da6468f25330522e92347c9156eb3f1cb95956/torch-2.9.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:c432d04376f6d9767a9852ea0def7b47a7bbc8e7af3b16ac9cf9ce02b12851c9", size = 899747132, upload-time = "2025-11-12T15:23:36.068Z" },
{ url = "https://files.pythonhosted.org/packages/63/5d/e8d4e009e52b6b2cf1684bde2a6be157b96fb873732542fb2a9a99e85a83/torch-2.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:d187566a2cdc726fc80138c3cdb260970fab1c27e99f85452721f7759bbd554d", size = 110934845, upload-time = "2025-11-12T15:22:48.367Z" },
{ url = "https://files.pythonhosted.org/packages/bd/b2/2d15a52516b2ea3f414643b8de68fa4cb220d3877ac8b1028c83dc8ca1c4/torch-2.9.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cb10896a1f7fedaddbccc2017ce6ca9ecaaf990f0973bdfcf405439750118d2c", size = 74823558, upload-time = "2025-11-12T15:22:43.392Z" },
{ url = "https://files.pythonhosted.org/packages/86/5c/5b2e5d84f5b9850cd1e71af07524d8cbb74cba19379800f1f9f7c997fc70/torch-2.9.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:0a2bd769944991c74acf0c4ef23603b9c777fdf7637f115605a4b2d8023110c7", size = 104145788, upload-time = "2025-11-12T15:23:52.109Z" },
{ url = "https://files.pythonhosted.org/packages/a9/8c/3da60787bcf70add986c4ad485993026ac0ca74f2fc21410bc4eb1bb7695/torch-2.9.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:07c8a9660bc9414c39cac530ac83b1fb1b679d7155824144a40a54f4a47bfa73", size = 899735500, upload-time = "2025-11-12T15:24:08.788Z" },
{ url = "https://files.pythonhosted.org/packages/db/2b/f7818f6ec88758dfd21da46b6cd46af9d1b3433e53ddbb19ad1e0da17f9b/torch-2.9.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c88d3299ddeb2b35dcc31753305612db485ab6f1823e37fb29451c8b2732b87e", size = 111163659, upload-time = "2025-11-12T15:23:20.009Z" },
]
[[package]]
@ -2541,6 +3190,10 @@ source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/72/ec90c3519eaf168f22cb1757ad412f3a2add4782ad3a92861c9ad135d886/triton-3.5.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61413522a48add32302353fdbaaf92daaaab06f6b5e3229940d21b5207f47579", size = 170425802, upload-time = "2025-11-11T17:40:53.209Z" },
{ url = "https://files.pythonhosted.org/packages/f2/50/9a8358d3ef58162c0a415d173cfb45b67de60176e1024f71fbc4d24c0b6d/triton-3.5.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d2c6b915a03888ab931a9fd3e55ba36785e1fe70cbea0b40c6ef93b20fc85232", size = 170470207, upload-time = "2025-11-11T17:41:00.253Z" },
{ url = "https://files.pythonhosted.org/packages/27/46/8c3bbb5b0a19313f50edcaa363b599e5a1a5ac9683ead82b9b80fe497c8d/triton-3.5.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3f4346b6ebbd4fad18773f5ba839114f4826037c9f2f34e0148894cd5dd3dba", size = 170470410, upload-time = "2025-11-11T17:41:06.319Z" },
{ url = "https://files.pythonhosted.org/packages/37/92/e97fcc6b2c27cdb87ce5ee063d77f8f26f19f06916aa680464c8104ef0f6/triton-3.5.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0b4d2c70127fca6a23e247f9348b8adde979d2e7a20391bfbabaac6aebc7e6a8", size = 170579924, upload-time = "2025-11-11T17:41:12.455Z" },
{ url = "https://files.pythonhosted.org/packages/a4/e6/c595c35e5c50c4bc56a7bac96493dad321e9e29b953b526bbbe20f9911d0/triton-3.5.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0637b1efb1db599a8e9dc960d53ab6e4637db7d4ab6630a0974705d77b14b60", size = 170480488, upload-time = "2025-11-11T17:41:18.222Z" },
{ url = "https://files.pythonhosted.org/packages/16/b5/b0d3d8b901b6a04ca38df5e24c27e53afb15b93624d7fd7d658c7cd9352a/triton-3.5.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bac7f7d959ad0f48c0e97d6643a1cc0fd5786fe61cb1f83b537c6b2d54776478", size = 170582192, upload-time = "2025-11-11T17:41:23.963Z" },
]
[[package]]
@ -2715,6 +3368,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
]
@ -2760,6 +3424,54 @@ wheels = [
{ 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" },
]
@ -2774,19 +3486,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/d4/c8/cc640404a0981e6c1
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/90/89a2ff242ccab6a24fbab18dbbabc67c51a6f0ed01f9a0f41689dc177419/yarg-0.1.9-py2.py3-none-any.whl", hash = "sha256:4f9cebdc00fac946c9bf2783d634e538a71c7d280a4d806d45fd4dc0ef441492", size = 19162, upload-time = "2014-08-11T22:01:41.104Z" },
]
[[package]]
name = "zep-cloud"
version = "3.25.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
{ name = "pydantic" },
{ name = "pydantic-core" },
{ name = "python-dateutil" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/81/0d/7a866e1e1e5f0f1353eedf3d0e17df9b3cf362c494bccd41eb121b49c43f/zep_cloud-3.25.0.tar.gz", hash = "sha256:a77867e02c2a9036a20623e85d03415e237e76510b6e05ff2e9ab2cdced0aeb9", size = 94772, upload-time = "2026-07-16T00:46:43.918Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/fc/5ca2bdd20b83bd62bc2708580f756e704f69704709e0ed43e735aa590c1e/zep_cloud-3.25.0-py3-none-any.whl", hash = "sha256:94d9599038b154af9ad33cab4b4873ed9adb1bcaa48fc4c41cbd407c1a657ef5", size = 166438, upload-time = "2026-07-16T00:46:42.39Z" },
]

View File

@ -1,7 +1,7 @@
services:
mirofish:
image: ghcr.io/666ghj/mirofish:latest
# 加速镜像(如拉取缓慢可替换上方地址)
# Mirror for faster pulling (replace the address above if pulling is slow)
# image: ghcr.nju.edu.cn/666ghj/mirofish:latest
container_name: mirofish
env_file:

View File

@ -1,15 +1,15 @@
<!doctype html>
<html lang="zh">
<html lang="en">
<head>
<script>document.documentElement.lang = localStorage.getItem('locale') || 'zh'</script>
<script>document.documentElement.lang = localStorage.getItem('locale') || 'en'</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@100..800&family=Noto+Sans+SC:wght@300;400;500;700;800;900&family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;1,9..144,300;1,9..144,400&family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@100..800&family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="MiroFish - 社交媒体舆论模拟系统" />
<title>MiroFish - 预测万物</title>
<meta name="description" content="MiroFish - Social Media Opinion Simulation System" />
<title>MiroFish - Predict Everything</title>
</head>
<body>
<div id="app"></div>

View File

@ -3,11 +3,11 @@
</template>
<script setup>
// 使 Vue Router
// Use Vue Router for page management
</script>
<style>
/* 全局样式重置 */
/* Global style reset */
* {
margin: 0;
padding: 0;
@ -15,33 +15,50 @@
}
#app {
font-family: 'JetBrains Mono', 'Space Grotesk', 'Noto Sans SC', monospace;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #000000;
background-color: #ffffff;
color: #0A1B2A;
background-color: #EBEDF0;
}
/* 滚动条样式 */
/* Scrollbar styles */
::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
background: #E4E7EC;
}
::-webkit-scrollbar-thumb {
background: #000000;
background: #56697D;
}
::-webkit-scrollbar-thumb:hover {
background: #333333;
background: #1E3247;
}
/* 全局按钮样式 */
/* Global button styles */
button {
font-family: inherit;
}
</style>
/* Global focus visible styles */
*:focus-visible {
outline: 2px solid #C4751A;
outline-offset: 2px;
}
/* Respect reduced-motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>

View File

@ -1,8 +1,8 @@
import service from './index'
/**
* 生成本体上传文档和模拟需求
* @param {Object} data - 包含files, simulation_requirement, project_name等
* Generate ontology (upload documents and simulation requirement)
* @param {Object} data - Contains files, simulation_requirement, project_name, etc.
* @returns {Promise}
*/
export function generateOntology(formData) {
@ -17,8 +17,8 @@ export function generateOntology(formData) {
}
/**
* 构建图谱
* @param {Object} data - 包含project_id, graph_name等
* Build graph
* @param {Object} data - Contains project_id, graph_name, etc.
* @returns {Promise}
*/
export function buildGraph(data) {
@ -30,8 +30,8 @@ export function buildGraph(data) {
}
/**
* 查询任务状态
* @param {String} taskId - 任务ID
* Query task status
* @param {String} taskId - Task ID
* @returns {Promise}
*/
export function getTaskStatus(taskId) {
@ -42,8 +42,8 @@ export function getTaskStatus(taskId) {
}
/**
* 获取图谱数据
* @param {String} graphId - 图谱ID
* Get graph data
* @param {String} graphId - Graph ID
* @returns {Promise}
*/
export function getGraphData(graphId) {
@ -54,8 +54,8 @@ export function getGraphData(graphId) {
}
/**
* 获取项目信息
* @param {String} projectId - 项目ID
* Get project info
* @param {String} projectId - Project ID
* @returns {Promise}
*/
export function getProject(projectId) {

View File

@ -1,16 +1,16 @@
import axios from 'axios'
import i18n from '../i18n'
// 创建axios实例
// Create axios instance
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5001',
timeout: 300000, // 5分钟超时(本体生成可能需要较长时间)
timeout: 300000, // 5 minute timeout (ontology generation may take longer)
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器
// Request interceptor
service.interceptors.request.use(
config => {
config.headers['Accept-Language'] = i18n.global.locale.value
@ -22,12 +22,12 @@ service.interceptors.request.use(
}
)
// 响应拦截器(容错重试机制)
// Response interceptor (fault-tolerant retry mechanism)
service.interceptors.response.use(
response => {
const res = response.data
// 如果返回的状态码不是success则抛出错误
// If returned status code is not success, throw error
if (!res.success && res.success !== undefined) {
console.error('API Error:', res.error || res.message || 'Unknown error')
return Promise.reject(new Error(res.error || res.message || 'Error'))
@ -38,12 +38,12 @@ service.interceptors.response.use(
error => {
console.error('Response error:', error)
// 处理超时
// Handle timeout
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
console.error('Request timeout')
}
// 处理网络错误
// Handle network error
if (error.message === 'Network Error') {
console.error('Network error - please check your connection')
}
@ -52,4 +52,18 @@ service.interceptors.response.use(
}
)
// Request function with retry
export const requestWithRetry = async (requestFn, maxRetries = 3, delay = 1000) => {
for (let i = 0; i < maxRetries; i++) {
try {
return await requestFn()
} catch (error) {
if (i === maxRetries - 1) throw error
console.warn(`Request failed, retrying (${i + 1}/${maxRetries})...`)
await new Promise(resolve => setTimeout(resolve, delay * Math.pow(2, i)))
}
}
}
export default service

View File

@ -1,7 +1,7 @@
import service from './index'
/**
* 开始报告生成
* Start report generation
* @param {Object} data - { simulation_id, force_regenerate? }
*/
export const generateReport = (data) => {
@ -9,7 +9,7 @@ export const generateReport = (data) => {
}
/**
* 获取报告生成状态
* Get report generation status
* @param {string} reportId
*/
export const getReportStatus = (reportId) => {
@ -17,25 +17,25 @@ export const getReportStatus = (reportId) => {
}
/**
* 获取 Agent 日志增量
* Get Agent log (incremental)
* @param {string} reportId
* @param {number} fromLine - 从第几行开始获取
* @param {number} fromLine - Starting line number
*/
export const getAgentLog = (reportId, fromLine = 0) => {
return service.get(`/api/report/${reportId}/agent-log`, { params: { from_line: fromLine } })
}
/**
* 获取控制台日志增量
* Get console log (incremental)
* @param {string} reportId
* @param {number} fromLine - 从第几行开始获取
* @param {number} fromLine - Starting line number
*/
export const getConsoleLog = (reportId, fromLine = 0) => {
return service.get(`/api/report/${reportId}/console-log`, { params: { from_line: fromLine } })
}
/**
* 获取报告详情
* Get report details
* @param {string} reportId
*/
export const getReport = (reportId) => {
@ -43,7 +43,7 @@ export const getReport = (reportId) => {
}
/**
* Report Agent 对话
* Chat with Report Agent
* @param {Object} data - { simulation_id, message, chat_history? }
*/
export const chatWithReport = (data) => {

View File

@ -1,23 +1,23 @@
import service from './index'
import service, { requestWithRetry } from './index'
/**
* 创建模拟
* Create simulation
* @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? }
*/
export const createSimulation = (data) => {
return service.post('/api/simulation/create', data)
return requestWithRetry(() => service.post('/api/simulation/create', data), 3, 1000)
}
/**
* 准备模拟环境异步任务
* Prepare simulation environment (async task)
* @param {Object} data - { simulation_id, entity_types?, use_llm_for_profiles?, parallel_profile_count?, force_regenerate? }
*/
export const prepareSimulation = (data) => {
return service.post('/api/simulation/prepare', data)
return requestWithRetry(() => service.post('/api/simulation/prepare', data), 3, 1000)
}
/**
* 查询准备任务进度
* Query preparation task progress
* @param {Object} data - { task_id?, simulation_id? }
*/
export const getPrepareStatus = (data) => {
@ -25,7 +25,7 @@ export const getPrepareStatus = (data) => {
}
/**
* 获取模拟状态
* Get simulation status
* @param {string} simulationId
*/
export const getSimulation = (simulationId) => {
@ -33,27 +33,25 @@ export const getSimulation = (simulationId) => {
}
/**
* 获取模拟的 Agent Profiles
* Get simulation Agent Profiles
* @param {string} simulationId
* @param {string} [platform] - 'reddit' | 'twitter'省略时由后端根据模拟配置自动选择
* @param {string} platform - 'reddit' | 'twitter'
*/
export const getSimulationProfiles = (simulationId, platform) => {
const params = platform ? { platform } : {}
return service.get(`/api/simulation/${simulationId}/profiles`, { params })
export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
return service.get(`/api/simulation/${simulationId}/profiles`, { params: { platform } })
}
/**
* 实时获取生成中的 Agent Profiles
* Get in-progress Agent Profiles in real-time
* @param {string} simulationId
* @param {string} [platform] - 'reddit' | 'twitter'省略时由后端根据模拟配置自动选择
* @param {string} platform - 'reddit' | 'twitter'
*/
export const getSimulationProfilesRealtime = (simulationId, platform) => {
const params = platform ? { platform } : {}
return service.get(`/api/simulation/${simulationId}/profiles/realtime`, { params })
export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit') => {
return service.get(`/api/simulation/${simulationId}/profiles/realtime`, { params: { platform } })
}
/**
* 获取模拟配置
* Get simulation config
* @param {string} simulationId
*/
export const getSimulationConfig = (simulationId) => {
@ -61,17 +59,17 @@ export const getSimulationConfig = (simulationId) => {
}
/**
* 实时获取生成中的模拟配置
* Get in-progress simulation config in real-time
* @param {string} simulationId
* @returns {Promise} 返回配置信息包含元数据和配置内容
* @returns {Promise} Returns config info including metadata and config content
*/
export const getSimulationConfigRealtime = (simulationId) => {
return service.get(`/api/simulation/${simulationId}/config/realtime`)
}
/**
* 列出所有模拟
* @param {string} projectId - 可选按项目ID过滤
* List all simulations
* @param {string} projectId - Optional, filter by project ID
*/
export const listSimulations = (projectId) => {
const params = projectId ? { project_id: projectId } : {}
@ -79,15 +77,15 @@ export const listSimulations = (projectId) => {
}
/**
* 启动模拟
* Start simulation
* @param {Object} data - { simulation_id, platform?, max_rounds?, enable_graph_memory_update? }
*/
export const startSimulation = (data) => {
return service.post('/api/simulation/start', data)
return requestWithRetry(() => service.post('/api/simulation/start', data), 3, 1000)
}
/**
* 停止模拟
* Stop simulation
* @param {Object} data - { simulation_id }
*/
export const stopSimulation = (data) => {
@ -95,7 +93,7 @@ export const stopSimulation = (data) => {
}
/**
* 获取模拟运行实时状态
* Get simulation run real-time status
* @param {string} simulationId
*/
export const getRunStatus = (simulationId) => {
@ -103,7 +101,7 @@ export const getRunStatus = (simulationId) => {
}
/**
* 获取模拟运行详细状态包含最近动作
* Get simulation run detailed status (including recent actions)
* @param {string} simulationId
*/
export const getRunStatusDetail = (simulationId) => {
@ -111,23 +109,23 @@ export const getRunStatusDetail = (simulationId) => {
}
/**
* 获取模拟中的帖子
* Get posts in simulation
* @param {string} simulationId
* @param {string} [platform] - 'reddit' | 'twitter'省略时由后端根据模拟配置自动选择
* @param {number} limit - 返回数量
* @param {number} offset - 偏移量
* @param {string} platform - 'reddit' | 'twitter'
* @param {number} limit - Return count
* @param {number} offset - Offset
*/
export const getSimulationPosts = (simulationId, platform, limit = 50, offset = 0) => {
const params = { limit, offset }
if (platform) params.platform = platform
return service.get(`/api/simulation/${simulationId}/posts`, { params })
export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50, offset = 0) => {
return service.get(`/api/simulation/${simulationId}/posts`, {
params: { platform, limit, offset }
})
}
/**
* 获取模拟时间线按轮次汇总
* Get simulation timeline (grouped by round)
* @param {string} simulationId
* @param {number} startRound - 起始轮次
* @param {number} endRound - 结束轮次
* @param {number} startRound - Start round
* @param {number} endRound - End round
*/
export const getSimulationTimeline = (simulationId, startRound = 0, endRound = null) => {
const params = { start_round: startRound }
@ -138,7 +136,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
}
/**
* 获取Agent统计信息
* Get Agent statistics
* @param {string} simulationId
*/
export const getAgentStats = (simulationId) => {
@ -146,7 +144,7 @@ export const getAgentStats = (simulationId) => {
}
/**
* 获取模拟动作历史
* Get simulation action history
* @param {string} simulationId
* @param {Object} params - { limit, offset, platform, agent_id, round_num }
*/
@ -155,7 +153,7 @@ export const getSimulationActions = (simulationId, params = {}) => {
}
/**
* 关闭模拟环境优雅退出
* Close simulation environment (graceful exit)
* @param {Object} data - { simulation_id, timeout? }
*/
export const closeSimulationEnv = (data) => {
@ -163,7 +161,7 @@ export const closeSimulationEnv = (data) => {
}
/**
* 获取模拟环境状态
* Get simulation environment status
* @param {Object} data - { simulation_id }
*/
export const getEnvStatus = (data) => {
@ -171,18 +169,19 @@ export const getEnvStatus = (data) => {
}
/**
* 批量采访 Agent
* Batch interview Agents
* @param {Object} data - { simulation_id, interviews: [{ agent_id, prompt }] }
*/
export const interviewAgents = (data) => {
return service.post('/api/simulation/interview/batch', data)
return requestWithRetry(() => service.post('/api/simulation/interview/batch', data), 3, 1000)
}
/**
* 获取历史模拟列表带项目详情
* 用于首页历史项目展示
* @param {number} limit - 返回数量限制
* Get historical simulation list (with project details)
* For home page historical project display
* @param {number} limit - Return count limit
*/
export const getSimulationHistory = (limit = 20) => {
return service.get('/api/simulation/history', { params: { limit } })
}

View File

@ -2,7 +2,7 @@
<div class="graph-panel">
<div class="panel-header">
<span class="panel-title">{{ $t('graph.panelTitle') }}</span>
<!-- 顶部工具栏 (Internal Top Right) -->
<!-- Top toolbar (Internal Top Right) -->
<div class="header-tools">
<button class="tool-btn" @click="$emit('refresh')" :disabled="loading" :title="$t('graph.refreshGraph')">
<span class="icon-refresh" :class="{ 'spinning': loading }"></span>
@ -15,11 +15,11 @@
</div>
<div class="graph-container" ref="graphContainer">
<!-- 图谱可视化 -->
<!-- Graph visualization -->
<div v-if="graphData" class="graph-view">
<svg ref="graphSvg" class="graph-svg"></svg>
<!-- 构建中/模拟中提示 -->
<!-- Building/simulating hint -->
<div v-if="currentPhase === 1 || isSimulating" class="graph-building-hint">
<div class="memory-icon-wrapper">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="memory-icon">
@ -30,7 +30,7 @@
{{ isSimulating ? $t('graph.graphMemoryRealtime') : $t('graph.realtimeUpdating') }}
</div>
<!-- 模拟结束后的提示 -->
<!-- Post-simulation hint -->
<div v-if="showSimulationFinishedHint" class="graph-building-hint finished-hint">
<div class="hint-icon-wrapper">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="hint-icon">
@ -48,7 +48,7 @@
</button>
</div>
<!-- 节点/边详情面板 -->
<!-- Node/Edge detail panel -->
<div v-if="selectedItem" class="detail-panel">
<div class="detail-panel-header">
<span class="detail-title">{{ selectedItem.type === 'node' ? $t('graph.nodeDetails') : $t('graph.relationship') }}</span>
@ -58,7 +58,7 @@
<button class="detail-close" @click="closeDetailPanel">×</button>
</div>
<!-- 节点详情 -->
<!-- Node details -->
<div v-if="selectedItem.type === 'node'" class="detail-content">
<div class="detail-row">
<span class="detail-label">Name:</span>
@ -101,9 +101,9 @@
</div>
</div>
<!-- 边详情 -->
<!-- Edge details -->
<div v-else class="detail-content">
<!-- 自环组详情 -->
<!-- Self-loop group details -->
<template v-if="selectedItem.data.isSelfLoopGroup">
<div class="edge-relation-header self-loop-header">
{{ selectedItem.data.source_name }} - Self Relations
@ -154,7 +154,7 @@
</div>
</template>
<!-- 普通边详情 -->
<!-- Regular edge details -->
<template v-else>
<div class="edge-relation-header">
{{ selectedItem.data.source_name }} {{ selectedItem.data.name || 'RELATED_TO' }} {{ selectedItem.data.target_name }}
@ -200,20 +200,20 @@
</div>
</div>
<!-- 加载状态 -->
<!-- Loading state -->
<div v-else-if="loading" class="graph-state">
<div class="loading-spinner"></div>
<p>{{ $t('graph.graphDataLoading') }}</p>
</div>
<!-- 等待/空状态 -->
<!-- Waiting/empty state -->
<div v-else class="graph-state">
<div class="empty-icon"></div>
<p class="empty-text">{{ $t('graph.waitingOntology') }}</p>
</div>
</div>
<!-- 底部图例 (Bottom Left) -->
<!-- Bottom legend (Bottom Left) -->
<div v-if="graphData && entityTypes.length" class="graph-legend">
<span class="legend-title">Entity Types</span>
<div class="legend-items">
@ -224,7 +224,7 @@
</div>
</div>
<!-- 显示边标签开关 -->
<!-- Show edge labels toggle -->
<div v-if="graphData" class="edge-labels-toggle">
<label class="toggle-switch">
<input type="checkbox" v-model="showEdgeLabels" />
@ -251,26 +251,26 @@ const emit = defineEmits(['refresh', 'toggle-maximize'])
const graphContainer = ref(null)
const graphSvg = ref(null)
const selectedItem = ref(null)
const showEdgeLabels = ref(true) //
const expandedSelfLoops = ref(new Set()) //
const showSimulationFinishedHint = ref(false) //
const wasSimulating = ref(false) //
const showEdgeLabels = ref(true) // Show edge labels by default
const expandedSelfLoops = ref(new Set()) // Expanded self-loop items
const showSimulationFinishedHint = ref(false) // Post-simulation hint
const wasSimulating = ref(false) // Track whether previously simulating
//
// Close post-simulation hint
const dismissFinishedHint = () => {
showSimulationFinishedHint.value = false
}
// isSimulating
// Watch isSimulating changes, detect simulation end
watch(() => props.isSimulating, (newValue, oldValue) => {
if (wasSimulating.value && !newValue) {
//
// Transition from simulating to non-simulating, show end hint
showSimulationFinishedHint.value = true
}
wasSimulating.value = newValue
}, { immediate: true })
// /
// Toggle self-loop item expand/collapse
const toggleSelfLoop = (id) => {
const newSet = new Set(expandedSelfLoops.value)
if (newSet.has(id)) {
@ -281,11 +281,11 @@ const toggleSelfLoop = (id) => {
expandedSelfLoops.value = newSet
}
//
// Calculate entity types for legend
const entityTypes = computed(() => {
if (!props.graphData?.nodes) return []
const typeMap = {}
//
// Beautiful color palette
const colors = ['#FF6B35', '#004E89', '#7B2D8E', '#1A936F', '#C5283D', '#E9724C', '#3498db', '#9b59b6', '#27ae60', '#f39c12']
props.graphData.nodes.forEach(node => {
@ -298,7 +298,7 @@ const entityTypes = computed(() => {
return Object.values(typeMap)
})
//
// Format time
const formatDateTime = (dateStr) => {
if (!dateStr) return ''
try {
@ -318,7 +318,7 @@ const formatDateTime = (dateStr) => {
const closeDetailPanel = () => {
selectedItem.value = null
expandedSelfLoops.value = new Set() //
expandedSelfLoops.value = new Set() // Reset expanded state
}
let currentSimulation = null
@ -328,7 +328,7 @@ let linkLabelBgRef = null
const renderGraph = () => {
if (!graphSvg.value || !props.graphData) return
// 仿
// Stop previous simulation
if (currentSimulation) {
currentSimulation.stop()
}
@ -362,16 +362,16 @@ const renderGraph = () => {
const nodeIds = new Set(nodes.map(n => n.id))
//
// Process edge data, calculate edge count and index between same node pair
const edgePairCount = {}
const selfLoopEdges = {} //
const selfLoopEdges = {} // Self-loop edges grouped by node
const tempEdges = edgesData
.filter(e => nodeIds.has(e.source_node_uuid) && nodeIds.has(e.target_node_uuid))
//
// Count edges between each node pair, collect self-loop edges
tempEdges.forEach(e => {
if (e.source_node_uuid === e.target_node_uuid) {
// -
// Self-loop - collect into array
if (!selfLoopEdges[e.source_node_uuid]) {
selfLoopEdges[e.source_node_uuid] = []
}
@ -386,9 +386,9 @@ const renderGraph = () => {
}
})
//
// Track which edge index for each node pair
const edgePairIndex = {}
const processedSelfLoopNodes = new Set() //
const processedSelfLoopNodes = new Set() // Processed self-loop nodes
const edges = []
@ -396,9 +396,9 @@ const renderGraph = () => {
const isSelfLoop = e.source_node_uuid === e.target_node_uuid
if (isSelfLoop) {
// -
// Self-loop edge - only add one merged self-loop per node
if (processedSelfLoopNodes.has(e.source_node_uuid)) {
return //
return // Already processed, skip
}
processedSelfLoopNodes.add(e.source_node_uuid)
@ -417,7 +417,7 @@ const renderGraph = () => {
source_name: nodeName,
target_name: nodeName,
selfLoopCount: allSelfLoops.length,
selfLoopEdges: allSelfLoops //
selfLoopEdges: allSelfLoops // Store all self-loop edge details
}
})
return
@ -428,19 +428,19 @@ const renderGraph = () => {
const currentIndex = edgePairIndex[pairKey] || 0
edgePairIndex[pairKey] = currentIndex + 1
// UUID < UUID
// Check if edge direction matches normalized direction (source UUID < target UUID)
const isReversed = e.source_node_uuid > e.target_node_uuid
// 线
// Calculate curvature: spread for multiple edges, straight for single edge
let curvature = 0
if (totalCount > 1) {
//
//
// Evenly distribute curvature, ensure clear distinction
// Curvature range increases with edge count, more edges = wider range
const curvatureRange = Math.min(1.2, 0.6 + totalCount * 0.15)
curvature = ((currentIndex / (totalCount - 1)) - 0.5) * curvatureRange * 2
//
//
// If edge direction is opposite to normalized, flip curvature
// This ensures all edges distribute in same reference frame, no overlap from direction differences
if (isReversed) {
curvature = -curvature
}
@ -468,11 +468,11 @@ const renderGraph = () => {
entityTypes.value.forEach(t => colorMap[t.name] = t.color)
const getColor = (type) => colorMap[type] || '#999'
// Simulation -
// Simulation - dynamically adjust node spacing based on edge count
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(edges).id(d => d.id).distance(d => {
//
// 150 40
// Dynamically adjust distance based on edge count between this node pair
// Base distance 150, +40 per additional edge
const baseDistance = 150
const edgeCount = d.pairTotal || 1
return baseDistance + (edgeCount - 1) * 50
@ -480,7 +480,7 @@ const renderGraph = () => {
.force('charge', d3.forceManyBody().strength(-400))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collide', d3.forceCollide(50))
//
// Add center gravity to pull isolated node clusters toward center
.force('x', d3.forceX(width / 2).strength(0.04))
.force('y', d3.forceY(height / 2).strength(0.04))
@ -493,39 +493,39 @@ const renderGraph = () => {
g.attr('transform', event.transform)
}))
// Links - 使 path 线
// Links - use path for curved support
const linkGroup = g.append('g').attr('class', 'links')
// 线
// Calculate curved path
const getLinkPath = (d) => {
const sx = d.source.x, sy = d.source.y
const tx = d.target.x, ty = d.target.y
//
// Detect self-loop
if (d.isSelfLoop) {
//
// Self-loop: draw an arc from node and back
const loopRadius = 30
//
const x1 = sx + 8 //
// Start from node right side, loop around and return
const x1 = sx + 8 // Start offset
const y1 = sy - 4
const x2 = sx + 8 //
const x2 = sx + 8 // End offset
const y2 = sy + 4
// 使sweep-flag=1
// Use arc for self-loop (sweep-flag=1 clockwise)
return `M${x1},${y1} A${loopRadius},${loopRadius} 0 1,1 ${x2},${y2}`
}
if (d.curvature === 0) {
// 线
// Straight line
return `M${sx},${sy} L${tx},${ty}`
}
// 线 -
// Calculate curve control point - dynamically adjust based on edge count and distance
const dx = tx - sx, dy = ty - sy
const dist = Math.sqrt(dx * dx + dy * dy)
// 线线
//
// Perpendicular offset from connection line, calculated by distance ratio, ensures visible curve
// More edges = larger offset ratio
const pairTotal = d.pairTotal || 1
const offsetRatio = 0.25 + pairTotal * 0.05 // 25%5%
const offsetRatio = 0.25 + pairTotal * 0.05 // Base 25%, +5% per additional edge
const baseOffset = Math.max(35, dist * offsetRatio)
const offsetX = -dy / dist * d.curvature * baseOffset
const offsetY = dx / dist * d.curvature * baseOffset
@ -535,14 +535,14 @@ const renderGraph = () => {
return `M${sx},${sy} Q${cx},${cy} ${tx},${ty}`
}
// 线
// Calculate curve midpoint (for label positioning)
const getLinkMidpoint = (d) => {
const sx = d.source.x, sy = d.source.y
const tx = d.target.x, ty = d.target.y
//
// Detect self-loop
if (d.isSelfLoop) {
//
// Self-loop label position: right of node
return { x: sx + 70, y: sy }
}
@ -550,7 +550,7 @@ const renderGraph = () => {
return { x: (sx + tx) / 2, y: (sy + ty) / 2 }
}
// 线 t=0.5
// Quadratic bezier midpoint t=0.5
const dx = tx - sx, dy = ty - sy
const dist = Math.sqrt(dx * dx + dy * dy)
const pairTotal = d.pairTotal || 1
@ -561,7 +561,7 @@ const renderGraph = () => {
const cx = (sx + tx) / 2 + offsetX
const cy = (sy + ty) / 2 + offsetY
// 线 B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2, t=0.5
// Quadratic bezier formula B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2, t=0.5
const midX = 0.25 * sx + 0.5 * cx + 0.25 * tx
const midY = 0.25 * sy + 0.5 * cy + 0.25 * ty
@ -577,11 +577,11 @@ const renderGraph = () => {
.style('cursor', 'pointer')
.on('click', (event, d) => {
event.stopPropagation()
//
// Reset previously selected edge style
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
linkLabels.attr('fill', '#666')
//
// Highlight currently selected edge
d3.select(event.target).attr('stroke', '#3498db').attr('stroke-width', 3)
selectedItem.value = {
@ -590,7 +590,7 @@ const renderGraph = () => {
}
})
// Link labels background (使)
// Link labels background (white background for clearer text)
const linkLabelBg = linkGroup.selectAll('rect')
.data(edges)
.enter().append('rect')
@ -605,7 +605,7 @@ const renderGraph = () => {
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
linkLabels.attr('fill', '#666')
//
// Highlight corresponding edge
link.filter(l => l === d).attr('stroke', '#3498db').attr('stroke-width', 3)
d3.select(event.target).attr('fill', 'rgba(52, 152, 219, 0.1)')
@ -633,7 +633,7 @@ const renderGraph = () => {
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
linkLabels.attr('fill', '#666')
//
// Highlight corresponding edge
link.filter(l => l === d).attr('stroke', '#3498db').attr('stroke-width', 3)
d3.select(event.target).attr('fill', '#3498db')
@ -643,7 +643,7 @@ const renderGraph = () => {
}
})
//
// Save reference for external show/hide control
linkLabelsRef = linkLabels
linkLabelBgRef = linkLabelBg
@ -661,7 +661,7 @@ const renderGraph = () => {
.style('cursor', 'pointer')
.call(d3.drag()
.on('start', (event, d) => {
// 仿
// Only record position, don't restart simulation (distinguish click vs drag)
d.fx = d.x
d.fy = d.y
d._dragStartX = event.x
@ -669,13 +669,13 @@ const renderGraph = () => {
d._isDragging = false
})
.on('drag', (event, d) => {
//
// Detect if actual drag started (moved beyond threshold)
const dx = event.x - d._dragStartX
const dy = event.y - d._dragStartY
const distance = Math.sqrt(dx * dx + dy * dy)
if (!d._isDragging && distance > 3) {
// 仿
// First detection of actual drag, then restart simulation
d._isDragging = true
simulation.alphaTarget(0.3).restart()
}
@ -686,7 +686,7 @@ const renderGraph = () => {
}
})
.on('end', (event, d) => {
// 仿
// Only gradually stop simulation if actual drag occurred
if (d._isDragging) {
simulation.alphaTarget(0)
}
@ -697,12 +697,12 @@ const renderGraph = () => {
)
.on('click', (event, d) => {
event.stopPropagation()
//
// Reset all node styles
node.attr('stroke', '#fff').attr('stroke-width', 2.5)
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
//
// Highlight selected node
d3.select(event.target).attr('stroke', '#E91E63').attr('stroke-width', 4)
//
// Highlight edges connected to this node
link.filter(l => l.source.id === d.id || l.target.id === d.id)
.attr('stroke', '#E91E63')
.attr('stroke-width', 2.5)
@ -739,19 +739,19 @@ const renderGraph = () => {
.style('font-family', 'system-ui, sans-serif')
simulation.on('tick', () => {
// 线
// Update curved paths
link.attr('d', d => getLinkPath(d))
//
// Update edge label positions (no rotation, horizontal for clarity)
linkLabels.each(function(d) {
const mid = getLinkMidpoint(d)
d3.select(this)
.attr('x', mid.x)
.attr('y', mid.y)
.attr('transform', '') //
.attr('transform', '') // Remove rotation, keep horizontal
})
//
// Update edge label background
linkLabelBg.each(function(d, i) {
const mid = getLinkMidpoint(d)
const textEl = linkLabels.nodes()[i]
@ -761,7 +761,7 @@ const renderGraph = () => {
.attr('y', mid.y - bbox.height / 2 - 2)
.attr('width', bbox.width + 8)
.attr('height', bbox.height + 4)
.attr('transform', '') //
.attr('transform', '') // Remove rotation
})
node
@ -773,7 +773,7 @@ const renderGraph = () => {
.attr('y', d => d.y)
})
//
// Click empty space to close detail panel
svg.on('click', () => {
selectedItem.value = null
node.attr('stroke', '#fff').attr('stroke-width', 2.5)
@ -787,7 +787,7 @@ watch(() => props.graphData, () => {
nextTick(renderGraph)
}, { deep: true })
//
// Watch edge label display toggle
watch(showEdgeLabels, (newVal) => {
if (linkLabelsRef) {
linkLabelsRef.style('display', newVal ? 'block' : 'none')
@ -1250,7 +1250,7 @@ input:checked + .slider:before {
50% { opacity: 1; transform: scale(1.15); filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); }
}
/* 模拟结束后的提示样式 */
/* Post-simulation hint styles */
.graph-building-hint.finished-hint {
background: rgba(0, 0, 0, 0.65);
border: 1px solid rgba(255, 255, 255, 0.1);

View File

@ -4,20 +4,20 @@
:class="{ 'no-projects': projects.length === 0 && !loading }"
ref="historyContainer"
>
<!-- 背景装饰技术网格线只在有项目时显示 -->
<!-- Background decoration: tech grid lines (only shown when projects exist) -->
<div v-if="projects.length > 0 || loading" class="tech-grid-bg">
<div class="grid-pattern"></div>
<div class="gradient-overlay"></div>
</div>
<!-- 标题区域 -->
<!-- Title area -->
<div class="section-header">
<div class="section-line"></div>
<span class="section-title">{{ $t('history.title') }}</span>
<div class="section-line"></div>
</div>
<!-- 卡片容器只在有项目时显示 -->
<!-- Card container (only shown when projects exist) -->
<div v-if="projects.length > 0" class="cards-container" :class="{ expanded: isExpanded }" :style="containerStyle">
<div
v-for="(project, index) in projects"
@ -29,7 +29,7 @@
@mouseleave="hoveringCard = null"
@click="navigateToProject(project)"
>
<!-- 卡片头部simulation_id 功能可用状态 -->
<!-- Card header: simulation_id and feature availability status -->
<div class="card-header">
<span class="card-id">{{ formatSimulationId(project.simulation_id) }}</span>
<div class="card-status-icons">
@ -50,12 +50,12 @@
</div>
</div>
<!-- 文件列表区域 -->
<!-- File list area -->
<div class="card-files-wrapper">
<!-- 角落装饰 - 取景框风格 -->
<!-- Corner decoration - viewfinder style -->
<div class="corner-mark top-left-only"></div>
<!-- 文件列表 -->
<!-- File list -->
<div class="files-list" v-if="project.files && project.files.length > 0">
<div
v-for="(file, fileIndex) in project.files.slice(0, 3)"
@ -65,25 +65,25 @@
<span class="file-tag" :class="getFileType(file.filename)">{{ getFileTypeLabel(file.filename) }}</span>
<span class="file-name">{{ truncateFilename(file.filename, 20) }}</span>
</div>
<!-- 如果有更多文件显示提示 -->
<!-- If more files exist, show hint -->
<div v-if="project.files.length > 3" class="files-more">
{{ $t('history.moreFiles', { count: project.files.length - 3 }) }}
</div>
</div>
<!-- 无文件时的占位 -->
<!-- Placeholder when no files -->
<div class="files-empty" v-else>
<span class="empty-file-icon"></span>
<span class="empty-file-text">{{ $t('history.noFiles') }}</span>
</div>
</div>
<!-- 卡片标题使用模拟需求的前20字作为标题 -->
<!-- Card title (first 20 chars of simulation requirement as title) -->
<h3 class="card-title">{{ getSimulationTitle(project.simulation_requirement) }}</h3>
<!-- 卡片描述模拟需求完整展示 -->
<!-- Card description (full simulation requirement) -->
<p class="card-desc">{{ truncateText(project.simulation_requirement, 55) }}</p>
<!-- 卡片底部 -->
<!-- Card bottom -->
<div class="card-footer">
<div class="card-datetime">
<span class="card-date">{{ formatDate(project.created_at) }}</span>
@ -94,23 +94,23 @@
</span>
</div>
<!-- 底部装饰线 (hover时展开) -->
<!-- Bottom decorative line (expands on hover) -->
<div class="card-bottom-line"></div>
</div>
</div>
<!-- 加载状态 -->
<!-- Loading state -->
<div v-if="loading" class="loading-state">
<span class="loading-spinner"></span>
<span class="loading-text">{{ $t('history.loadingText') }}</span>
</div>
<!-- 历史回放详情弹窗 -->
<!-- History replay detail modal -->
<Teleport to="body">
<Transition name="modal">
<div v-if="selectedProject" class="modal-overlay" @click.self="closeModal">
<div class="modal-content">
<!-- 弹窗头部 -->
<!-- Modal header -->
<div class="modal-header">
<div class="modal-title-section">
<span class="modal-id">{{ formatSimulationId(selectedProject.simulation_id) }}</span>
@ -122,15 +122,15 @@
<button class="modal-close" @click="closeModal">×</button>
</div>
<!-- 弹窗内容 -->
<!-- Modal content -->
<div class="modal-body">
<!-- 模拟需求 -->
<!-- Simulation requirement -->
<div class="modal-section">
<div class="modal-label">{{ $t('history.simRequirement') }}</div>
<div class="modal-requirement">{{ selectedProject.simulation_requirement || $t('common.none') }}</div>
</div>
<!-- 文件列表 -->
<!-- File list -->
<div class="modal-section">
<div class="modal-label">{{ $t('history.relatedFiles') }}</div>
<div class="modal-files" v-if="selectedProject.files && selectedProject.files.length > 0">
@ -143,14 +143,14 @@
</div>
</div>
<!-- 推演回放分割线 -->
<!-- Simulation replay divider -->
<div class="modal-divider">
<span class="divider-line"></span>
<span class="divider-text">{{ $t('history.replayTitle') }}</span>
<span class="divider-line"></span>
</div>
<!-- 导航按钮 -->
<!-- Navigation buttons -->
<div class="modal-actions">
<button
class="modal-btn btn-project"
@ -179,7 +179,7 @@
<span class="btn-text">{{ $t('history.step4Button') }}</span>
</button>
</div>
<!-- 不可回放提示 -->
<!-- Non-replayable hint -->
<div class="modal-playback-hint">
<span class="hint-text">{{ $t('history.replayHint') }}</span>
</div>
@ -200,56 +200,56 @@ const router = useRouter()
const route = useRoute()
const { t } = useI18n()
//
// State
const projects = ref([])
const loading = ref(true)
const isExpanded = ref(false)
const hoveringCard = ref(null)
const historyContainer = ref(null)
const selectedProject = ref(null) //
const selectedProject = ref(null) // Currently selected project (for modal)
let observer = null
let isAnimating = false //
let expandDebounceTimer = null //
let pendingState = null //
let isAnimating = false // Animation lock, prevents flicker
let expandDebounceTimer = null // Debounce timer
let pendingState = null // Record pending target state
// -
// Card layout config - adjusted for wider ratio
const CARDS_PER_ROW = 4
const CARD_WIDTH = 280
const CARD_HEIGHT = 280
const CARD_GAP = 24
//
// Dynamically calculate container height style
const containerStyle = computed(() => {
if (!isExpanded.value) {
//
// Collapsed: fixed height
return { minHeight: '420px' }
}
//
// Expanded: dynamically calculate height based on card count
const total = projects.value.length
if (total === 0) {
return { minHeight: '280px' }
}
const rows = Math.ceil(total / CARDS_PER_ROW)
// * + (-1) * +
// Calculate actual needed height: rows * card height + (rows-1) * gap + small bottom padding
const expandedHeight = rows * CARD_HEIGHT + (rows - 1) * CARD_GAP + 10
return { minHeight: `${expandedHeight}px` }
})
//
// Get card style
const getCardStyle = (index) => {
const total = projects.value.length
if (isExpanded.value) {
//
// Expanded: grid layout
const transition = 'transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease, border-color 0.3s ease'
const col = index % CARDS_PER_ROW
const row = Math.floor(index / CARDS_PER_ROW)
//
// Calculate cards per row, ensure centered
const currentRowStart = row * CARDS_PER_ROW
const currentRowCards = Math.min(CARDS_PER_ROW, total - currentRowStart)
@ -259,7 +259,7 @@ const getCardStyle = (index) => {
const colInRow = index % CARDS_PER_ROW
const x = startX + colInRow * (CARD_WIDTH + CARD_GAP)
//
// Expand downward, increase spacing from title
const y = 20 + row * (CARD_HEIGHT + CARD_GAP)
return {
@ -269,14 +269,14 @@ const getCardStyle = (index) => {
transition: transition
}
} else {
//
// Collapsed: fan stack
const transition = 'transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease, border-color 0.3s ease'
const centerIndex = (total - 1) / 2
const offset = index - centerIndex
const x = offset * 35
//
// Adjust start position, close to title but with proper spacing
const y = 25 + Math.abs(offset) * 8
const r = offset * 3
const s = 0.95 - Math.abs(offset) * 0.05
@ -290,24 +290,24 @@ const getCardStyle = (index) => {
}
}
//
// Get style class based on round progress
const getProgressClass = (simulation) => {
const current = simulation.current_round || 0
const total = simulation.total_rounds || 0
if (total === 0 || current === 0) {
//
// Not started
return 'not-started'
} else if (current >= total) {
//
// Completed
return 'completed'
} else {
//
// In progress
return 'in-progress'
}
}
//
// Format date (show date portion only)
const formatDate = (dateStr) => {
if (!dateStr) return ''
try {
@ -324,7 +324,7 @@ const formatDate = (dateStr) => {
}
}
// :
// Format time (show HH:MM)
const formatTime = (dateStr) => {
if (!dateStr) return ''
try {
@ -337,27 +337,27 @@ const formatTime = (dateStr) => {
}
}
//
// Truncate text
const truncateText = (text, maxLength) => {
if (!text) return ''
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
}
// 20
// Generate title from simulation requirement (first 20 chars)
const getSimulationTitle = (requirement) => {
if (!requirement) return t('history.untitledSimulation')
const title = requirement.slice(0, 20)
return requirement.length > 20 ? title + '...' : title
}
// simulation_id 6
// Format simulation_id display (first 6 chars)
const formatSimulationId = (simulationId) => {
if (!simulationId) return 'SIM_UNKNOWN'
const prefix = simulationId.replace('sim_', '').slice(0, 6)
return `SIM_${prefix.toUpperCase()}`
}
// /
// Format round display (current/total)
const formatRounds = (simulation) => {
const current = simulation.current_round || 0
const total = simulation.total_rounds || 0
@ -365,7 +365,7 @@ const formatRounds = (simulation) => {
return t('history.roundsProgress', { current, total })
}
//
// Get file type (for styling)
const getFileType = (filename) => {
if (!filename) return 'other'
const ext = filename.split('.').pop()?.toLowerCase()
@ -381,14 +381,14 @@ const getFileType = (filename) => {
return typeMap[ext] || 'other'
}
//
// Get file type label text
const getFileTypeLabel = (filename) => {
if (!filename) return 'FILE'
const ext = filename.split('.').pop()?.toUpperCase()
return ext || 'FILE'
}
//
// Truncate filename (preserve extension)
const truncateFilename = (filename, maxLength) => {
if (!filename) return t('history.unknownFile')
if (filename.length <= maxLength) return filename
@ -399,17 +399,17 @@ const truncateFilename = (filename, maxLength) => {
return truncatedName + ext
}
//
// Open project detail modal
const navigateToProject = (simulation) => {
selectedProject.value = simulation
}
//
// Close modal
const closeModal = () => {
selectedProject.value = null
}
// Project
// Navigate to graph build page (Project)
const goToProject = () => {
if (selectedProject.value?.project_id) {
router.push({
@ -420,7 +420,7 @@ const goToProject = () => {
}
}
// Simulation
// Navigate to environment config page (Simulation)
const goToSimulation = () => {
if (selectedProject.value?.simulation_id) {
router.push({
@ -431,7 +431,7 @@ const goToSimulation = () => {
}
}
// Report
// Navigate to analysis report page (Report)
const goToReport = () => {
if (selectedProject.value?.report_id) {
router.push({
@ -442,7 +442,7 @@ const goToReport = () => {
}
}
//
// Load historical projects
const loadHistory = async () => {
try {
loading.value = true
@ -451,14 +451,14 @@ const loadHistory = async () => {
projects.value = response.data || []
}
} catch (error) {
console.error('加载历史项目失败:', error)
console.error('Failed to load historical projects:', error)
projects.value = []
} finally {
loading.value = false
}
}
// IntersectionObserver
// Initialize IntersectionObserver
const initObserver = () => {
if (observer) {
observer.disconnect()
@ -469,47 +469,47 @@ const initObserver = () => {
entries.forEach((entry) => {
const shouldExpand = entry.isIntersecting
//
// Update pending target state (always record latest target state regardless of animation)
pendingState = shouldExpand
//
// Clear previous debounce timer (new scroll intent overrides old)
if (expandDebounceTimer) {
clearTimeout(expandDebounceTimer)
expandDebounceTimer = null
}
//
// If animating, only record state, process after animation ends
if (isAnimating) return
//
// If target state equals current state, no processing needed
if (shouldExpand === isExpanded.value) {
pendingState = null
return
}
// 使
// (50ms)(200ms)
// Use debounce to delay state switch, prevent rapid flicker
// Shorter delay on expand (50ms), longer on collapse (200ms) for stability
const delay = shouldExpand ? 50 : 200
expandDebounceTimer = setTimeout(() => {
//
// Check if currently animating
if (isAnimating) return
//
// Check if pending state still needs execution (may have been overridden by subsequent scroll)
if (pendingState === null || pendingState === isExpanded.value) return
//
// Set animation lock
isAnimating = true
isExpanded.value = pendingState
pendingState = null
//
// Release lock after animation, check for pending state changes
setTimeout(() => {
isAnimating = false
//
// After animation ends, check for new pending state
if (pendingState !== null && pendingState !== isExpanded.value) {
//
// Delay briefly before executing, avoid too-rapid switching
expandDebounceTimer = setTimeout(() => {
if (pendingState !== null && pendingState !== isExpanded.value) {
isAnimating = true
@ -526,20 +526,20 @@ const initObserver = () => {
})
},
{
// 使使
// Use multiple thresholds for smoother detection
threshold: [0.4, 0.6, 0.8],
// rootMargin
// Adjust rootMargin, shrink viewport bottom, require more scroll to trigger expand
rootMargin: '0px 0px -150px 0px'
}
)
//
// Start observing
if (historyContainer.value) {
observer.observe(historyContainer.value)
}
}
//
// Watch route changes, reload data when returning to home
watch(() => route.path, (newPath) => {
if (newPath === '/') {
loadHistory()
@ -547,28 +547,28 @@ watch(() => route.path, (newPath) => {
})
onMounted(async () => {
// DOM
// Ensure DOM renders before loading data
await nextTick()
await loadHistory()
// DOM
// Wait for DOM render then init observer
setTimeout(() => {
initObserver()
}, 100)
})
// 使 keep-alive
// If using keep-alive, reload data on component activation
onActivated(() => {
loadHistory()
})
onUnmounted(() => {
// Intersection Observer
// Clean up Intersection Observer
if (observer) {
observer.disconnect()
observer = null
}
//
// Clean up debounce timer
if (expandDebounceTimer) {
clearTimeout(expandDebounceTimer)
expandDebounceTimer = null
@ -577,7 +577,7 @@ onUnmounted(() => {
</script>
<style scoped>
/* 容器 */
/* Container */
.history-database {
position: relative;
width: 100%;
@ -587,13 +587,13 @@ onUnmounted(() => {
overflow: visible;
}
/* 无项目时简化显示 */
/* Simplified display when no projects */
.history-database.no-projects {
min-height: auto;
padding: 40px 0 20px;
}
/* 技术网格背景 */
/* Tech grid background */
.tech-grid-bg {
position: absolute;
top: 0;
@ -604,7 +604,7 @@ onUnmounted(() => {
pointer-events: none;
}
/* 使用CSS背景图案创建固定间距的正方形网格 */
/* Use CSS background pattern to create fixed-spacing square grid */
.grid-pattern {
position: absolute;
top: 0;
@ -615,7 +615,7 @@ onUnmounted(() => {
linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
background-size: 50px 50px;
/* 从左上角开始定位,高度变化时只在底部扩展,不影响已有网格位置 */
/* Position from top-left, only expand at bottom on height change, preserve existing grid positions */
background-position: top left;
}
@ -631,7 +631,7 @@ onUnmounted(() => {
pointer-events: none;
}
/* 标题区域 */
/* Title area */
.section-header {
position: relative;
z-index: 100;
@ -659,7 +659,7 @@ onUnmounted(() => {
text-transform: uppercase;
}
/* 卡片容器 */
/* Card container */
.cards-container {
position: relative;
display: flex;
@ -667,10 +667,10 @@ onUnmounted(() => {
align-items: flex-start;
padding: 0 40px;
transition: min-height 700ms cubic-bezier(0.23, 1, 0.32, 1);
/* min-height 由 JS 动态计算,根据卡片数量自适应 */
/* min-height dynamically calculated by JS, adapts to card count */
}
/* 项目卡片 */
/* Project card */
.project-card {
position: absolute;
width: 280px;
@ -693,7 +693,7 @@ onUnmounted(() => {
z-index: 1000 !important;
}
/* 卡片头部 */
/* Card header */
.card-header {
display: flex;
justify-content: space-between;
@ -711,7 +711,7 @@ onUnmounted(() => {
font-weight: 500;
}
/* 功能状态图标组 */
/* Feature status icon group */
.card-status-icons {
display: flex;
align-items: center;
@ -728,17 +728,17 @@ onUnmounted(() => {
opacity: 1;
}
/* 不同功能的颜色 */
.status-icon:nth-child(1).available { color: #3B82F6; } /* 图谱构建 - 蓝色 */
.status-icon:nth-child(2).available { color: #F59E0B; } /* 环境搭建 - 橙色 */
.status-icon:nth-child(3).available { color: #10B981; } /* 分析报告 - 绿色 */
/* Colors for different features */
.status-icon:nth-child(1).available { color: #3B82F6; } /* Graph Build - blue */
.status-icon:nth-child(2).available { color: #F59E0B; } /* Env Setup - orange */
.status-icon:nth-child(3).available { color: #10B981; } /* Analysis Report - green */
.status-icon.unavailable {
color: #D1D5DB;
opacity: 0.5;
}
/* 轮数进度显示 */
/* Round progress display */
.card-progress {
display: flex;
align-items: center;
@ -752,13 +752,13 @@ onUnmounted(() => {
font-size: 0.5rem;
}
/* 进度状态颜色 */
.card-progress.completed { color: #10B981; } /* 已完成 - 绿色 */
.card-progress.in-progress { color: #F59E0B; } /* 进行中 - 橙色 */
.card-progress.not-started { color: #9CA3AF; } /* 未开始 - 灰色 */
/* Progress status colors */
.card-progress.completed { color: #10B981; } /* Completed - green */
.card-progress.in-progress { color: #F59E0B; } /* In Progress - orange */
.card-progress.not-started { color: #9CA3AF; } /* Not Started - gray */
.card-status.pending { color: #9CA3AF; }
/* 文件列表区域 */
/* File list area */
.card-files-wrapper {
position: relative;
width: 100%;
@ -778,7 +778,7 @@ onUnmounted(() => {
gap: 4px;
}
/* 更多文件提示 */
/* More files hint */
.files-more {
display: flex;
align-items: center;
@ -808,7 +808,7 @@ onUnmounted(() => {
border-color: #e5e7eb;
}
/* 简约文件标签样式 */
/* Minimal file tag styles */
.file-tag {
display: inline-flex;
align-items: center;
@ -826,7 +826,7 @@ onUnmounted(() => {
min-width: 28px;
}
/* 低饱和度配色方案 - Morandi色系 */
/* Low saturation color scheme - Morandi palette */
.file-tag.pdf { background: #f2e6e6; color: #a65a5a; }
.file-tag.doc { background: #e6eff5; color: #5a7ea6; }
.file-tag.xls { background: #e6f2e8; color: #5aa668; }
@ -847,7 +847,7 @@ onUnmounted(() => {
letter-spacing: 0.1px;
}
/* 无文件时的占位 */
/* Placeholder when no files */
.files-empty {
display: flex;
align-items: center;
@ -868,13 +868,13 @@ onUnmounted(() => {
letter-spacing: 0.5px;
}
/* 悬停时文件区域效果 */
/* File area hover effect */
.project-card:hover .card-files-wrapper {
border-color: #d1d5db;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
}
/* 角落装饰 */
/* Corner decoration */
.corner-mark.top-left-only {
position: absolute;
top: 6px;
@ -887,7 +887,7 @@ onUnmounted(() => {
z-index: 10;
}
/* 卡片标题 */
/* Card title */
.card-title {
font-family: 'Inter', -apple-system, sans-serif;
font-size: 0.9rem;
@ -905,7 +905,7 @@ onUnmounted(() => {
color: #2563EB;
}
/* 卡片描述 */
/* Card description */
.card-desc {
font-family: 'Inter', sans-serif;
font-size: 0.75rem;
@ -919,7 +919,7 @@ onUnmounted(() => {
-webkit-box-orient: vertical;
}
/* 卡片底部 */
/* Card bottom */
.card-footer {
position: relative;
display: flex;
@ -933,14 +933,14 @@ onUnmounted(() => {
font-weight: 500;
}
/* 日期时间组合 */
/* Date/time combo */
.card-datetime {
display: flex;
align-items: center;
gap: 8px;
}
/* 底部轮数进度显示 */
/* Bottom round progress display */
.card-footer .card-progress {
display: flex;
align-items: center;
@ -954,12 +954,12 @@ onUnmounted(() => {
font-size: 0.5rem;
}
/* 进度状态颜色 - 底部 */
/* Progress status colors - bottom */
.card-footer .card-progress.completed { color: #10B981; }
.card-footer .card-progress.in-progress { color: #F59E0B; }
.card-footer .card-progress.not-started { color: #9CA3AF; }
/* 底部装饰线 */
/* Bottom decorative line */
.card-bottom-line {
position: absolute;
bottom: 0;
@ -975,7 +975,7 @@ onUnmounted(() => {
width: 100%;
}
/* 空状态 */
/* Empty state */
.empty-state, .loading-state {
display: flex;
flex-direction: column;
@ -1003,7 +1003,7 @@ onUnmounted(() => {
to { transform: rotate(360deg); }
}
/* 响应式 */
/* Responsive */
@media (max-width: 1200px) {
.project-card {
width: 240px;
@ -1019,7 +1019,7 @@ onUnmounted(() => {
}
}
/* ===== 历史回放详情弹窗样式 ===== */
/* ===== History Replay Detail Modal Styles ===== */
.modal-overlay {
position: fixed;
top: 0;
@ -1045,7 +1045,7 @@ onUnmounted(() => {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* 动画过渡 */
/* Animation transition */
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
@ -1074,7 +1074,7 @@ onUnmounted(() => {
opacity: 0;
}
/* 弹窗头部 */
/* Modal header */
.modal-header {
display: flex;
justify-content: space-between;
@ -1141,7 +1141,7 @@ onUnmounted(() => {
color: #111827;
}
/* 弹窗内容 */
/* Modal content */
.modal-body {
padding: 24px 32px;
}
@ -1183,7 +1183,7 @@ onUnmounted(() => {
padding-right: 4px;
}
/* 自定义滚动条样式 */
/* Custom scrollbar styles */
.modal-files::-webkit-scrollbar {
width: 4px;
}
@ -1237,7 +1237,7 @@ onUnmounted(() => {
text-align: center;
}
/* 推演回放分割线 */
/* Simulation replay divider */
.modal-divider {
display: flex;
align-items: center;
@ -1261,7 +1261,7 @@ onUnmounted(() => {
white-space: nowrap;
}
/* 导航按钮 */
/* Navigation buttons */
.modal-actions {
display: flex;
gap: 16px;
@ -1328,7 +1328,7 @@ onUnmounted(() => {
color: #111827;
}
/* 不可回放提示 */
/* Non-replayable hint */
.modal-playback-hint {
display: flex;
align-items: center;

View File

@ -210,10 +210,10 @@ const selectedOntologyItem = ref(null)
const logContent = ref(null)
const creatingSimulation = ref(false)
// - simulation
// Enter environment setup - create simulation and navigate
const handleEnterEnvSetup = async () => {
if (!props.projectData?.project_id || !props.projectData?.graph_id) {
console.error('缺少项目或图谱信息')
console.error('Missing project or graph info')
return
}
@ -228,17 +228,17 @@ const handleEnterEnvSetup = async () => {
})
if (res.success && res.data?.simulation_id) {
// simulation
// Navigate to simulation page
router.push({
name: 'Simulation',
params: { simulationId: res.data.simulation_id }
})
} else {
console.error('创建模拟失败:', res.error)
console.error('Failed to create simulation:', res.error)
alert(t('step1.createSimulationFailed', { error: res.error || t('common.unknownError') }))
}
} catch (err) {
console.error('创建模拟异常:', err)
console.error('Simulation creation error:', err)
alert(t('step1.createSimulationException', { error: err.message }))
} finally {
creatingSimulation.value = false

View File

@ -1,7 +1,7 @@
<template>
<div class="env-setup-panel">
<div class="scroll-container">
<!-- Step 01: 模拟实例 -->
<!-- Step 01: Simulation Instance -->
<div class="step-card" :class="{ 'active': phase === 0, 'completed': phase > 0 }">
<div class="card-header">
<div class="step-info">
@ -41,7 +41,7 @@
</div>
</div>
<!-- Step 02: 生成 Agent 人设 -->
<!-- Step 02: Generate Agent Personas -->
<div class="step-card" :class="{ 'active': phase === 1, 'completed': phase > 1 }">
<div class="card-header">
<div class="step-info">
@ -113,7 +113,7 @@
</div>
</div>
<!-- Step 03: 生成双平台模拟配置 -->
<!-- Step 03: Generate Dual-Platform Config -->
<div class="step-card" :class="{ 'active': phase === 2, 'completed': phase > 2 }">
<div class="card-header">
<div class="step-info">
@ -135,7 +135,7 @@
<!-- Config Preview -->
<div v-if="simulationConfig" class="config-detail-panel">
<!-- 时间配置 -->
<!-- Time Config -->
<div class="config-block">
<div class="config-grid">
<div class="config-item">
@ -179,7 +179,7 @@
</div>
</div>
<!-- Agent 配置 -->
<!-- Agent Config -->
<div class="config-block">
<div class="config-block-header">
<span class="config-block-title">{{ $t('step2.agentConfig') }}</span>
@ -191,7 +191,7 @@
:key="agent.agent_id"
class="agent-card"
>
<!-- 卡片头部 -->
<!-- Card header -->
<div class="agent-card-header">
<div class="agent-identity">
<span class="agent-id">Agent {{ agent.agent_id }}</span>
@ -203,7 +203,7 @@
</div>
</div>
<!-- 活跃时间轴 -->
<!-- Activity timeline -->
<div class="agent-timeline">
<span class="timeline-label">{{ $t('step2.activeTimePeriod') }}</span>
<div class="mini-timeline">
@ -224,7 +224,7 @@
</div>
</div>
<!-- 行为参数 -->
<!-- Behavior params -->
<div class="agent-params">
<div class="param-group">
<div class="param-item">
@ -264,7 +264,7 @@
</div>
</div>
<!-- 平台配置 -->
<!-- Platform Config -->
<div class="config-block">
<div class="config-block-header">
<span class="config-block-title">{{ $t('step2.recommendAlgoConfig') }}</span>
@ -327,7 +327,7 @@
</div>
</div>
<!-- LLM 配置推理 -->
<!-- LLM Config Reasoning -->
<div v-if="simulationConfig.generation_reasoning" class="config-block">
<div class="config-block-header">
<span class="config-block-title">{{ $t('step2.llmConfigReasoning') }}</span>
@ -346,7 +346,7 @@
</div>
</div>
<!-- Step 04: 初始激活编排 -->
<!-- Step 04: Initial Activation Orchestration -->
<div class="step-card" :class="{ 'active': phase === 3, 'completed': phase > 3 }">
<div class="card-header">
<div class="step-info">
@ -367,7 +367,7 @@
</p>
<div v-if="simulationConfig?.event_config" class="orchestration-content">
<!-- 叙事方向 -->
<!-- Narrative Direction -->
<div class="narrative-box">
<span class="box-label narrative-label">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="special-icon">
@ -385,7 +385,7 @@
<p class="narrative-text">{{ simulationConfig.event_config.narrative_direction }}</p>
</div>
<!-- 热点话题 -->
<!-- Hot Topics -->
<div class="topics-section">
<span class="box-label">{{ $t('step2.initialHotTopics') }}</span>
<div class="hot-topics-grid">
@ -395,7 +395,7 @@
</div>
</div>
<!-- 初始帖子流 -->
<!-- Initial Post Stream -->
<div class="initial-posts-section">
<span class="box-label">{{ $t('step2.initialActivationSeq', { count: simulationConfig.event_config.initial_posts.length }) }}</span>
<div class="posts-timeline">
@ -418,7 +418,7 @@
</div>
</div>
<!-- Step 05: 准备完成 -->
<!-- Step 05: Setup Complete -->
<div class="step-card" :class="{ 'active': phase === 4 }">
<div class="card-header">
<div class="step-info">
@ -435,7 +435,7 @@
<p class="api-note">POST /api/simulation/start</p>
<p class="description">{{ $t('step2.setupCompleteDesc') }}</p>
<!-- 模拟轮数配置 - 只有在配置生成完成且轮数计算出来后才显示 -->
<!-- Simulation rounds config - only shown after config generation and rounds calculated -->
<div v-if="simulationConfig && autoGeneratedRounds" class="rounds-config-section">
<div class="rounds-header">
<div class="header-left">
@ -544,7 +544,7 @@
</div>
<div class="modal-body">
<!-- 基本信息 -->
<!-- Basic Info -->
<div class="modal-info-grid">
<div class="info-item">
<span class="info-label">{{ $t('step2.profileModalAge') }}</span>
@ -564,13 +564,13 @@
</div>
</div>
<!-- 简介 -->
<!-- Bio -->
<div class="modal-section">
<span class="section-label">{{ $t('step2.profileModalBio') }}</span>
<p class="section-bio">{{ selectedProfile.bio || $t('step2.noBio') }}</p>
</div>
<!-- 关注话题 -->
<!-- Followed Topics -->
<div class="modal-section" v-if="selectedProfile.interested_topics?.length">
<span class="section-label">{{ $t('step2.profileModalTopics') }}</span>
<div class="topics-grid">
@ -582,11 +582,11 @@
</div>
</div>
<!-- 详细人设 -->
<!-- Detailed Persona -->
<div class="modal-section" v-if="selectedProfile.persona">
<span class="section-label">{{ $t('step2.profileModalPersona') }}</span>
<!-- 人设维度概览 -->
<!-- Persona Dimension Overview -->
<div class="persona-dimensions">
<div class="dimension-card">
<span class="dim-title">{{ $t('step2.personaDimExperience') }}</span>
@ -645,7 +645,7 @@ import {
const { t } = useI18n()
const props = defineProps({
simulationId: String, //
simulationId: String, // Passed from parent component
projectData: Object,
graphData: Object,
systemLogs: Array
@ -654,7 +654,7 @@ const props = defineProps({
const emit = defineEmits(['go-back', 'next-step', 'add-log', 'update-status'])
// State
const phase = ref(0) // 0: , 1: , 2: , 3:
const phase = ref(0) // 0: Init, 1: Generating personas, 2: Generating config, 3: Complete
const taskId = ref(null)
const prepareProgress = ref(0)
const currentStage = ref('')
@ -666,43 +666,43 @@ const simulationConfig = ref(null)
const selectedProfile = ref(null)
const showProfilesDetail = ref(true)
//
// Log dedup: record last output key info
let lastLoggedMessage = ''
let lastLoggedProfileCount = 0
let lastLoggedConfigStage = ''
//
const useCustomRounds = ref(false) // 使
const customMaxRounds = ref(40) // 40
// Simulation rounds config
const useCustomRounds = ref(false) // Default: use auto-configured rounds
const customMaxRounds = ref(40) // Default recommended: 40 rounds
// Watch stage to update phase
watch(currentStage, (newStage) => {
if (newStage === '生成Agent人设' || newStage === 'generating_profiles') {
if (newStage === 'Generating Agent Personas' || newStage === 'generating_profiles') {
phase.value = 1
} else if (newStage === '生成模拟配置' || newStage === 'generating_config') {
} else if (newStage === 'Generating Simulation Config' || newStage === 'generating_config') {
phase.value = 2
//
// Enter config generation phase, start polling config
if (!configTimer) {
addLog(t('log.startGeneratingConfig'))
startConfigPolling()
}
} else if (newStage === '准备模拟脚本' || newStage === 'copying_scripts') {
phase.value = 2 //
} else if (newStage === 'Preparing Simulation Scripts' || newStage === 'copying_scripts') {
phase.value = 2 // Still in config phase
}
})
// 使
// Calculate auto-generated rounds from config (no hardcoded defaults)
const autoGeneratedRounds = computed(() => {
if (!simulationConfig.value?.time_config) {
return null // null
return null // Return null when config not yet generated
}
const totalHours = simulationConfig.value.time_config.total_simulation_hours
const minutesPerRound = simulationConfig.value.time_config.minutes_per_round
if (!totalHours || !minutesPerRound) {
return null // null
return null // Return null when config data incomplete
}
const calculatedRounds = Math.floor((totalHours * 60) / minutesPerRound)
// 40
// Ensure max rounds >= 40 (recommended), avoid slider range anomaly
return Math.max(calculatedRounds, 40)
})
@ -719,7 +719,7 @@ const displayProfiles = computed(() => {
return profiles.value.slice(0, 6)
})
// agent_idusername
// Get corresponding username by agent_id
const getAgentUsername = (agentId) => {
if (profiles.value && profiles.value.length > agentId && agentId >= 0) {
const profile = profiles.value[agentId]
@ -728,7 +728,7 @@ const getAgentUsername = (agentId) => {
return `agent_${agentId}`
}
//
// Calculate total related topics across all personas
const totalTopicsCount = computed(() => {
return profiles.value.reduce((sum, p) => {
return sum + (p.interested_topics?.length || 0)
@ -748,17 +748,17 @@ const handlePrepareFailure = (message) => {
emit('update-status', 'error')
}
//
// Handle start simulation button click
const handleStartSimulation = () => {
//
// Build params to pass to parent component
const params = {}
if (useCustomRounds.value) {
// max_rounds
// User custom rounds, pass max_rounds param
params.maxRounds = customMaxRounds.value
addLog(t('log.startSimCustomRounds', { rounds: customMaxRounds.value }))
} else {
// max_rounds
// User chose auto-generated rounds, don't pass max_rounds param
addLog(t('log.startSimAutoRounds', { rounds: autoGeneratedRounds.value }))
}
@ -776,7 +776,7 @@ const selectProfile = (profile) => {
selectedProfile.value = profile
}
//
// Auto-start simulation preparation
const startPrepareSimulation = async () => {
if (!props.simulationId) {
addLog(t('log.errorMissingSimId'))
@ -784,7 +784,7 @@ const startPrepareSimulation = async () => {
return
}
//
// Mark step 1 complete, start step 2
phase.value = 1
addLog(t('log.simInstanceCreated', { id: props.simulationId }))
addLog(t('log.preparingSimEnv'))
@ -808,19 +808,19 @@ const startPrepareSimulation = async () => {
addLog(t('log.prepareTaskStarted'))
addLog(t('log.prepareTaskId', { taskId: res.data.task_id }))
// Agentprepare
// Immediately set expected Agent total (from prepare API return value)
if (res.data.expected_entities_count) {
expectedTotal.value = res.data.expected_entities_count
addLog(t('log.zepEntitiesFound', { count: res.data.expected_entities_count }))
addLog(t('log.graphEntitiesFound', { count: res.data.expected_entities_count }))
if (res.data.entity_types && res.data.entity_types.length > 0) {
addLog(t('log.entityTypes', { types: res.data.entity_types.join(', ') }))
}
}
addLog(t('log.startPollingProgress'))
//
// Start polling progress
startPolling()
// Profiles
// Start real-time Profile fetching
startProfilesPolling()
} else {
addLog(t('log.prepareFailed', { error: res.error || t('common.unknownError') }))
@ -866,15 +866,15 @@ const pollPrepareStatus = async () => {
if (res.success && res.data) {
const data = res.data
//
// Update progress
prepareProgress.value = data.progress || 0
progressMessage.value = data.message || ''
//
// Parse phase info and output detailed log
if (data.progress_detail) {
currentStage.value = data.progress_detail.current_stage_name || ''
//
// Output detailed progress log (avoid duplicates)
const detail = data.progress_detail
const logKey = `${detail.current_stage}-${detail.current_item}-${detail.total_items}`
if (logKey !== lastLoggedMessage && detail.item_description) {
@ -887,19 +887,19 @@ const pollPrepareStatus = async () => {
}
}
} else if (data.message) {
//
// Extract phase from message
const match = data.message.match(/\[(\d+)\/(\d+)\]\s*([^:]+)/)
if (match) {
currentStage.value = match[3].trim()
}
//
// Output message log (avoid duplicates)
if (data.message !== lastLoggedMessage) {
lastLoggedMessage = data.message
addLog(data.message)
}
}
//
// Check if complete
if (data.status === 'completed' || data.status === 'ready' || data.already_prepared) {
addLog(t('log.prepareComplete'))
stopPolling()
@ -910,7 +910,7 @@ const pollPrepareStatus = async () => {
}
}
} catch (err) {
console.warn('轮询状态失败:', err)
console.warn('Poll status failed:', err)
}
}
@ -923,19 +923,19 @@ const fetchProfilesRealtime = async () => {
if (res.success && res.data) {
const prevCount = profiles.value.length
profiles.value = res.data.profiles || []
// API
// Only update when API returns valid value, avoid overwriting existing valid value
if (res.data.total_expected) {
expectedTotal.value = res.data.total_expected
}
//
// Extract entity types
const types = new Set()
profiles.value.forEach(p => {
if (p.entity_type) types.add(p.entity_type)
})
entityTypes.value = Array.from(types)
// Profile
// Output Profile generation progress log (only when count changes)
const currentCount = profiles.value.length
if (currentCount > 0 && currentCount !== lastLoggedProfileCount) {
lastLoggedProfileCount = currentCount
@ -947,18 +947,18 @@ const fetchProfilesRealtime = async () => {
}
addLog(t('log.agentProfile', { current: currentCount, total: total, name: profileName, profession: latestProfile?.profession || t('step2.unknownProfession') }))
//
// If all generation complete
if (expectedTotal.value && currentCount >= expectedTotal.value) {
addLog(t('log.allProfilesComplete', { count: currentCount }))
}
}
}
} catch (err) {
console.warn('获取 Profiles 失败:', err)
console.warn('Failed to get Profiles:', err)
}
}
//
// Config polling
const startConfigPolling = () => {
configTimer = setInterval(fetchConfigRealtime, 2000)
}
@ -984,7 +984,7 @@ const fetchConfigRealtime = async () => {
return
}
//
// Output config generation phase log (avoid duplicates)
if (data.generation_stage && data.generation_stage !== lastLoggedConfigStage) {
lastLoggedConfigStage = data.generation_stage
if (data.generation_stage === 'generating_profiles') {
@ -994,12 +994,12 @@ const fetchConfigRealtime = async () => {
}
}
//
// If config generated
if (data.config_generated && data.config) {
simulationConfig.value = data.config
addLog(t('log.configComplete'))
//
// Show detailed config summary
if (data.summary) {
addLog(t('log.configSummaryAgents', { count: data.summary.total_agents }))
addLog(t('log.configSummaryHours', { hours: data.summary.simulation_hours }))
@ -1008,13 +1008,13 @@ const fetchConfigRealtime = async () => {
addLog(t('log.configSummaryPlatforms', { twitter: data.summary.has_twitter_config ? '✓' : '✗', reddit: data.summary.has_reddit_config ? '✓' : '✗' }))
}
//
// Show time config details
if (data.config.time_config) {
const tc = data.config.time_config
addLog(t('log.timeConfigDetail', { minutes: tc.minutes_per_round, rounds: Math.floor((tc.total_simulation_hours * 60) / tc.minutes_per_round) }))
}
//
// Show event config
if (data.config.event_config?.narrative_direction) {
const narrative = data.config.event_config.narrative_direction
addLog(t('log.narrativeDirection', { direction: narrative.length > 50 ? narrative.substring(0, 50) + '...' : narrative }))
@ -1027,7 +1027,7 @@ const fetchConfigRealtime = async () => {
}
}
} catch (err) {
console.warn('获取 Config 失败:', err)
console.warn('Failed to get Config:', err)
}
}
@ -1035,11 +1035,11 @@ const loadPreparedData = async () => {
phase.value = 2
addLog(t('log.loadingExistingConfig'))
// Profiles
// Final Profile fetch
await fetchProfilesRealtime()
addLog(t('log.loadedAgentProfiles', { count: profiles.value.length }))
// 使
// Get config (using real-time API)
try {
const res = await getSimulationConfigRealtime(props.simulationId)
if (res.success && res.data) {
@ -1054,7 +1054,7 @@ const loadPreparedData = async () => {
simulationConfig.value = configState.config
addLog(t('log.configLoadSuccess'))
//
// Show detailed config summary
if (configState.summary) {
addLog(t('log.configSummaryAgents', { count: configState.summary.total_agents }))
addLog(t('log.configSummaryHours', { hours: configState.summary.simulation_hours }))
@ -1087,7 +1087,7 @@ watch(() => props.systemLogs?.length, () => {
})
onMounted(() => {
//
// Auto-start preparation flow
if (props.simulationId) {
addLog(t('log.step2Init'))
startPrepareSimulation()
@ -1923,7 +1923,7 @@ onUnmounted(() => {
flex: 1;
}
/* 基本信息网格 */
/* Basic info grid */
.modal-info-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
@ -1959,7 +1959,7 @@ onUnmounted(() => {
color: #FF5722;
}
/* 模块区域 */
/* Module area */
.modal-section {
margin-bottom: 28px;
}
@ -1985,7 +1985,7 @@ onUnmounted(() => {
border-left: 3px solid #E0E0E0;
}
/* 话题标签 */
/* Topic tags */
.topics-grid {
display: flex;
flex-wrap: wrap;
@ -2007,7 +2007,7 @@ onUnmounted(() => {
color: #0D47A1;
}
/* 详细人设 */
/* Detailed persona */
.persona-dimensions {
display: grid;
grid-template-columns: repeat(2, 1fr);
@ -2293,7 +2293,7 @@ onUnmounted(() => {
margin: 0;
}
/* 模拟轮数配置样式 */
/* Simulation rounds config styles */
.rounds-config-section {
margin: 24px 0;
padding-top: 24px;

View File

@ -3,7 +3,7 @@
<!-- Top Control Bar -->
<div class="control-bar">
<div class="status-group">
<!-- Twitter 平台进度 -->
<!-- Twitter Platform Progress -->
<div class="platform-status twitter" :class="{ active: runStatus.twitter_running, completed: runStatus.twitter_completed }">
<div class="platform-header">
<svg class="platform-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
@ -30,7 +30,7 @@
<span class="stat-value mono">{{ runStatus.twitter_actions_count || 0 }}</span>
</span>
</div>
<!-- 可用动作提示 -->
<!-- Available actions hint -->
<div class="actions-tooltip">
<div class="tooltip-title">Available Actions</div>
<div class="tooltip-actions">
@ -44,7 +44,7 @@
</div>
</div>
<!-- Reddit 平台进度 -->
<!-- Reddit Platform Progress -->
<div class="platform-status reddit" :class="{ active: runStatus.reddit_running, completed: runStatus.reddit_completed }">
<div class="platform-header">
<svg class="platform-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
@ -71,7 +71,7 @@
<span class="stat-value mono">{{ runStatus.reddit_actions_count || 0 }}</span>
</span>
</div>
<!-- 可用动作提示 -->
<!-- Available actions hint -->
<div class="actions-tooltip">
<div class="tooltip-title">Available Actions</div>
<div class="tooltip-actions">
@ -157,12 +157,12 @@
</div>
<div class="card-body">
<!-- CREATE_POST: 发布帖子 -->
<!-- CREATE_POST: Create post -->
<div v-if="action.action_type === 'CREATE_POST' && action.action_args?.content" class="content-text main-text">
{{ action.action_args.content }}
</div>
<!-- QUOTE_POST: 引用帖子 -->
<!-- QUOTE_POST: Quote post -->
<template v-if="action.action_type === 'QUOTE_POST'">
<div v-if="action.action_args?.quote_content" class="content-text">
{{ action.action_args.quote_content }}
@ -178,7 +178,7 @@
</div>
</template>
<!-- REPOST: 转发帖子 -->
<!-- REPOST: Repost -->
<template v-if="action.action_type === 'REPOST'">
<div class="repost-info">
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
@ -189,7 +189,7 @@
</div>
</template>
<!-- LIKE_POST: 点赞帖子 -->
<!-- LIKE_POST: Like post -->
<template v-if="action.action_type === 'LIKE_POST'">
<div class="like-info">
<svg class="icon-small filled" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
@ -200,7 +200,7 @@
</div>
</template>
<!-- CREATE_COMMENT: 发表评论 -->
<!-- CREATE_COMMENT: Create comment -->
<template v-if="action.action_type === 'CREATE_COMMENT'">
<div v-if="action.action_args?.content" class="content-text">
{{ action.action_args.content }}
@ -211,7 +211,7 @@
</div>
</template>
<!-- SEARCH_POSTS: 搜索帖子 -->
<!-- SEARCH_POSTS: Search posts -->
<template v-if="action.action_type === 'SEARCH_POSTS'">
<div class="search-info">
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
@ -220,7 +220,7 @@
</div>
</template>
<!-- FOLLOW: 关注用户 -->
<!-- FOLLOW: Follow user -->
<template v-if="action.action_type === 'FOLLOW'">
<div class="follow-info">
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>
@ -240,7 +240,7 @@
</div>
</template>
<!-- DO_NOTHING: 无操作静默 -->
<!-- DO_NOTHING: No action (silent) -->
<template v-if="action.action_type === 'DO_NOTHING'">
<div class="idle-info">
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
@ -248,7 +248,7 @@
</div>
</template>
<!-- 通用回退未知类型或有 content 但未被上述处理 -->
<!-- Generic fallback: unknown type or has content but not handled above -->
<div v-if="!['CREATE_POST', 'QUOTE_POST', 'REPOST', 'LIKE_POST', 'CREATE_COMMENT', 'SEARCH_POSTS', 'FOLLOW', 'UPVOTE_POST', 'DOWNVOTE_POST', 'DO_NOTHING'].includes(action.action_type) && action.action_args?.content" class="content-text">
{{ action.action_args.content }}
</div>
@ -301,10 +301,10 @@ const { t } = useI18n()
const props = defineProps({
simulationId: String,
maxRounds: Number, // Step2
maxRounds: Number, // Max rounds passed from Step2
minutesPerRound: {
type: Number,
default: 30 // 30
default: 30 // Default: 30 minutes per round
},
projectData: Object,
graphData: Object,
@ -317,22 +317,22 @@ const router = useRouter()
// State
const isGeneratingReport = ref(false)
const phase = ref(0) // 0: , 1: , 2:
const phase = ref(0) // 0: Not started, 1: Running, 2: Completed
const isStarting = ref(false)
const isStopping = ref(false)
const startError = ref(null)
const runStatus = ref({})
const allActions = ref([]) //
const actionIds = ref(new Set()) // ID
const allActions = ref([]) // All actions (incremental accumulation)
const actionIds = ref(new Set()) // Action ID set for deduplication
const scrollContainer = ref(null)
// Computed
//
// Show actions in chronological order (newest at bottom)
const chronologicalActions = computed(() => {
return allActions.value
})
//
// Per-platform action counts
const twitterActionsCount = computed(() => {
return allActions.value.filter(a => a.platform === 'twitter').length
})
@ -341,7 +341,7 @@ const redditActionsCount = computed(() => {
return allActions.value.filter(a => a.platform === 'reddit').length
})
//
// Format simulated elapsed time (calculated from rounds and minutes per round)
const formatElapsedTime = (currentRound) => {
if (!currentRound || currentRound <= 0) return '0h 0m'
const totalMinutes = currentRound * props.minutesPerRound
@ -350,12 +350,12 @@ const formatElapsedTime = (currentRound) => {
return `${hours}h ${minutes}m`
}
// Twitter
// Twitter platform simulated elapsed time
const twitterElapsedTime = computed(() => {
return formatElapsedTime(runStatus.value.twitter_current_round || 0)
})
// Reddit
// Reddit platform simulated elapsed time
const redditElapsedTime = computed(() => {
return formatElapsedTime(runStatus.value.reddit_current_round || 0)
})
@ -365,7 +365,7 @@ const addLog = (msg) => {
emit('add-log', msg)
}
//
// Reset all state (for simulation restart)
const resetAllState = () => {
phase.value = 0
runStatus.value = {}
@ -376,17 +376,17 @@ const resetAllState = () => {
startError.value = null
isStarting.value = false
isStopping.value = false
stopPolling() //
stopPolling() // Stop any previous polling
}
//
// Start simulation
const doStartSimulation = async () => {
if (!props.simulationId) {
addLog(t('log.errorMissingSimId'))
return
}
//
// Reset all state first to avoid interference from previous simulation
resetAllState()
isStarting.value = true
@ -398,8 +398,8 @@ const doStartSimulation = async () => {
const params = {
simulation_id: props.simulationId,
platform: 'parallel',
force: true, //
enable_graph_memory_update: true //
force: true, // Force restart
enable_graph_memory_update: true // Enable dynamic graph update
}
if (props.maxRounds) {
@ -424,7 +424,7 @@ const doStartSimulation = async () => {
startStatusPolling()
startDetailPolling()
} else {
startError.value = res.error || '启动失败'
startError.value = res.error || 'Start failed'
addLog(t('log.startFailed', { error: res.error || t('common.unknownError') }))
emit('update-status', 'error')
}
@ -437,7 +437,7 @@ const doStartSimulation = async () => {
}
}
//
// Stop simulation
const handleStopSimulation = async () => {
if (!props.simulationId) return
@ -462,7 +462,7 @@ const handleStopSimulation = async () => {
}
}
//
// Poll status
let statusTimer = null
let detailTimer = null
@ -485,7 +485,7 @@ const stopPolling = () => {
}
}
//
// Track last round per platform for change detection and logging
const prevTwitterRound = ref(0)
const prevRedditRound = ref(0)
@ -500,7 +500,7 @@ const fetchRunStatus = async () => {
runStatus.value = data
//
// Detect round changes per platform and log
if (data.twitter_current_round > prevTwitterRound.value) {
addLog(`[Plaza] R${data.twitter_current_round}/${data.total_rounds} | T:${data.twitter_simulated_hours || 0}h | A:${data.twitter_actions_count}`)
prevTwitterRound.value = data.twitter_current_round
@ -511,18 +511,23 @@ const fetchRunStatus = async () => {
prevRedditRound.value = data.reddit_current_round
}
// runner_status
// Check if simulation completed (via runner_status or platform completion status)
const isCompleted = data.runner_status === 'completed' || data.runner_status === 'stopped'
const isFailed = data.runner_status === 'failed'
// runner_status is authoritative because the backend only publishes a
// terminal state after the Zep ingestion barrier has completed.
// Extra check: if backend hasn't updated runner_status yet but platform reports completed
// Detect via twitter_completed and reddit_completed status
const platformsCompleted = checkPlatformsCompleted(data)
if (isFailed) {
addLog(t('log.simFailed') + (data.error ? `: ${data.error}` : ''))
phase.value = 2
stopPolling()
emit('update-status', 'error')
} else if (isCompleted) {
} else if (isCompleted || platformsCompleted) {
if (platformsCompleted && !isCompleted) {
addLog(t('log.allPlatformsCompleted'))
}
addLog(t('log.simCompleted'))
phase.value = 2
stopPolling()
@ -530,28 +535,28 @@ const fetchRunStatus = async () => {
}
}
} catch (err) {
console.warn('获取运行状态失败:', err)
console.warn('Failed to get run state:', err)
}
}
//
// Check if all enabled platforms have completed
const checkPlatformsCompleted = (data) => {
// false
// If no platform data, return false
if (!data) return false
//
// Check completion status per platform
const twitterCompleted = data.twitter_completed === true
const redditCompleted = data.reddit_completed === true
//
// actions_count count > 0 running true
// If at least one platform completed, check if all enabled platforms completed
// Determine if platform is enabled via actions_count (if count > 0 or running was true)
const twitterEnabled = (data.twitter_actions_count > 0) || data.twitter_running || twitterCompleted
const redditEnabled = (data.reddit_actions_count > 0) || data.reddit_running || redditCompleted
// false
// If no platform is enabled, return false
if (!twitterEnabled && !redditEnabled) return false
//
// Check if all enabled platforms have completed
if (twitterEnabled && !twitterCompleted) return false
if (redditEnabled && !redditCompleted) return false
@ -565,13 +570,13 @@ const fetchRunStatusDetail = async () => {
const res = await getRunStatusDetail(props.simulationId)
if (res.success && res.data) {
// 使 all_actions
// Use all_actions to get full action list
const serverActions = res.data.all_actions || []
//
// Incrementally add new actions (dedup)
let newActionsAdded = 0
serverActions.forEach(action => {
// ID
// Generate unique ID
const actionId = action.id || `${action.timestamp}-${action.platform}-${action.agent_id}-${action.action_type}`
if (!actionIds.value.has(actionId)) {
@ -584,11 +589,11 @@ const fetchRunStatusDetail = async () => {
}
})
//
//
// Don't auto-scroll, let user freely browse timeline
// New actions appended at bottom
}
} catch (err) {
console.warn('获取详细状态失败:', err)
console.warn('Failed to get detailed status:', err)
}
}
@ -666,7 +671,7 @@ const handleNextStep = async () => {
const reportId = res.data.report_id
addLog(t('log.reportGenTaskStarted', { reportId }))
//
// Navigate to report page
router.push({ name: 'Report', params: { reportId } })
} else {
addLog(t('log.reportGenFailed', { error: res.error || t('common.unknownError') }))

View File

@ -127,7 +127,7 @@
</div>
</div>
<!-- Next Step Button - 在完成后显示 -->
<!-- Next Step Button - shown after completion -->
<button v-if="isComplete" class="next-step-btn" @click="goToInteraction">
<span>{{ $t('step4.goToInteraction') }}</span>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
@ -194,7 +194,7 @@
</div>
</template>
<!-- Section Content Generated (内容生成完成但整个章节可能还没完成) -->
<!-- Section Content Generated (content generation complete, but the entire section may not be done yet) -->
<template v-if="log.action === 'section_content'">
<div class="section-tag content-ready">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
@ -205,7 +205,7 @@
</div>
</template>
<!-- Section Complete (章节生成完成) -->
<!-- Section Complete (section generation complete) -->
<template v-if="log.action === 'section_complete'">
<div class="section-tag completed">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
@ -315,7 +315,7 @@
Final: {{ log.details?.has_final_answer ? 'Yes' : 'No' }}
</span>
</div>
<!-- 当是最终答案时显示特殊提示 -->
<!-- When it's the final answer, show a special hint -->
<div v-if="log.details?.has_final_answer" class="final-answer-hint">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
@ -433,22 +433,22 @@ const showRawResult = reactive({})
// Toggle functions
const toggleRawResult = (timestamp, event) => {
//
// Save the button's position relative to the viewport
const button = event?.target
const buttonRect = button?.getBoundingClientRect()
const buttonTopBeforeToggle = buttonRect?.top
//
// Toggle state
showRawResult[timestamp] = !showRawResult[timestamp]
// DOM
// Wait for DOM update, then adjust scroll position to keep the button in the same position
if (button && buttonTopBeforeToggle !== undefined && rightPanel.value) {
nextTick(() => {
const newButtonRect = button.getBoundingClientRect()
const buttonTopAfterToggle = newButtonRect.top
const scrollDelta = buttonTopAfterToggle - buttonTopBeforeToggle
//
// Adjust scroll position
rightPanel.value.scrollTop += scrollDelta
})
}
@ -466,7 +466,7 @@ const toggleSectionContent = (idx) => {
}
const toggleSectionCollapse = (idx) => {
//
// Only completed sections can be collapsed
if (!generatedSections.value[idx + 1]) return
const newSet = new Set(collapsedSections.value)
if (newSet.has(idx)) {
@ -499,32 +499,32 @@ const toolConfig = {
'insight_forge': {
name: 'Deep Insight',
color: 'purple',
icon: 'lightbulb' // -
icon: 'lightbulb' // lightbulb icon - represents insight
},
'panorama_search': {
name: 'Panorama Search',
color: 'blue',
icon: 'globe' // -
icon: 'globe' // globe icon - represents panoramic search
},
'interview_agents': {
name: 'Agent Interview',
color: 'green',
icon: 'users' // -
icon: 'users' // users icon - represents conversation
},
'quick_search': {
name: 'Quick Search',
color: 'orange',
icon: 'zap' // -
icon: 'zap' // lightning icon - represents speed
},
'get_graph_statistics': {
name: 'Graph Stats',
color: 'cyan',
icon: 'chart' // -
icon: 'chart' // chart icon - represents statistics
},
'get_entities_by_type': {
name: 'Entity Query',
color: 'pink',
icon: 'database' // -
icon: 'database' // Database icon - represents entities
}
}
@ -553,31 +553,31 @@ const parseInsightForge = (text) => {
}
try {
//
const queryMatch = text.match(/分析问题:\s*(.+?)(?:\n|$)/)
// Extract analysis question
const queryMatch = text.match(/Analysis Question:\s*(.+?)(?:\n|$)/)
if (queryMatch) result.query = queryMatch[1].trim()
//
const reqMatch = text.match(/预测场景:\s*(.+?)(?:\n|$)/)
// Extract prediction scenario
const reqMatch = text.match(/Prediction Scenario:\s*(.+?)(?:\n|$)/)
if (reqMatch) result.simulationRequirement = reqMatch[1].trim()
// - ": X"
const factMatch = text.match(/相关预测事实:\s*(\d+)/)
const entityMatch = text.match(/涉及实体:\s*(\d+)/)
const relMatch = text.match(/关系链:\s*(\d+)/)
// Extract statistics - match "Related prediction facts: X items" format
const factMatch = text.match(/Related Prediction Facts:\s*(\d+)/)
const entityMatch = text.match(/Entities Involved:\s*(\d+)/)
const relMatch = text.match(/Relationship Chains:\s*(\d+)/)
if (factMatch) result.stats.facts = parseInt(factMatch[1])
if (entityMatch) result.stats.entities = parseInt(entityMatch[1])
if (relMatch) result.stats.relationships = parseInt(relMatch[1])
// -
const subQSection = text.match(/### 分析的子问题\n([\s\S]*?)(?=\n###|$)/)
// Extract sub-questions - full extraction, no limit
const subQSection = text.match(/### Analysis Sub-Questions\n([\s\S]*?)(?=\n###|$)/)
if (subQSection) {
const lines = subQSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.subQueries = lines.map(l => l.replace(/^\d+\.\s*/, '').trim()).filter(Boolean)
}
// -
const factsSection = text.match(/### 【关键事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
// Extract key facts - full extraction, no limit
const factsSection = text.match(/### [Key Facts][\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
if (factsSection) {
const lines = factsSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.facts = lines.map(l => {
@ -586,16 +586,16 @@ const parseInsightForge = (text) => {
}).filter(Boolean)
}
// -
const entitySection = text.match(/### 【核心实体】\n([\s\S]*?)(?=\n###|$)/)
// Extract core entities - full extraction, including summary and related fact count
const entitySection = text.match(/### [Core Entities]\n([\s\S]*?)(?=\n###|$)/)
if (entitySection) {
const entityText = entitySection[1]
// "- **"
// Split entity blocks by "- **"
const entityBlocks = entityText.split(/\n(?=- \*\*)/).filter(b => b.trim().startsWith('- **'))
result.entities = entityBlocks.map(block => {
const nameMatch = block.match(/^-\s*\*\*(.+?)\*\*\s*\((.+?)\)/)
const summaryMatch = block.match(/摘要:\s*"?(.+?)"?(?:\n|$)/)
const relatedMatch = block.match(/相关事实:\s*(\d+)/)
const summaryMatch = block.match(/Summary:\s*"?(.+?)"?(?:\n|$)/)
const relatedMatch = block.match(/Related Facts:\s*(\d+)/)
return {
name: nameMatch ? nameMatch[1].trim() : '',
type: nameMatch ? nameMatch[2].trim() : '',
@ -605,8 +605,8 @@ const parseInsightForge = (text) => {
}).filter(e => e.name)
}
// -
const relSection = text.match(/### 【关系链】\n([\s\S]*?)(?=\n###|$)/)
// Extract relationship chains - full extraction, no limit
const relSection = text.match(/### [Relationship Chains]\n([\s\S]*?)(?=\n###|$)/)
if (relSection) {
const lines = relSection[1].split('\n').filter(l => l.trim().startsWith('-'))
result.relations = lines.map(l => {
@ -634,33 +634,33 @@ const parsePanorama = (text) => {
}
try {
//
const queryMatch = text.match(/查询:\s*(.+?)(?:\n|$)/)
// Extract query
const queryMatch = text.match(/Query:\s*(.+?)(?:\n|$)/)
if (queryMatch) result.query = queryMatch[1].trim()
//
const nodesMatch = text.match(/总节点数:\s*(\d+)/)
const edgesMatch = text.match(/总边数:\s*(\d+)/)
const activeMatch = text.match(/当前有效事实:\s*(\d+)/)
const histMatch = text.match(/历史\/过期事实:\s*(\d+)/)
// Extract statistics
const nodesMatch = text.match(/Total Nodes:\s*(\d+)/)
const edgesMatch = text.match(/Total Edges:\s*(\d+)/)
const activeMatch = text.match(/Currently Active Facts:\s*(\d+)/)
const histMatch = text.match(/Historical\/Expired Facts:\s*(\d+)/)
if (nodesMatch) result.stats.nodes = parseInt(nodesMatch[1])
if (edgesMatch) result.stats.edges = parseInt(edgesMatch[1])
if (activeMatch) result.stats.activeFacts = parseInt(activeMatch[1])
if (histMatch) result.stats.historicalFacts = parseInt(histMatch[1])
// -
const activeSection = text.match(/### 【当前有效事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
// Extract currently active facts - full extraction, no limit
const activeSection = text.match(/### [Currently Active Facts][\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
if (activeSection) {
const lines = activeSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.activeFacts = lines.map(l => {
//
// Remove numbering and quotes
const factText = l.replace(/^\d+\.\s*/, '').replace(/^"|"$/g, '').trim()
return factText
}).filter(Boolean)
}
// / -
const histSection = text.match(/### 【历史\/过期事实】[\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
// Extract historical/expired facts - full extraction, no limit
const histSection = text.match(/### [Historical\/Expired Facts][\s\S]*?\n([\s\S]*?)(?=\n###|$)/)
if (histSection) {
const lines = histSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.historicalFacts = lines.map(l => {
@ -669,8 +669,8 @@ const parsePanorama = (text) => {
}).filter(Boolean)
}
// -
const entitySection = text.match(/### 【涉及实体】\n([\s\S]*?)(?=\n###|$)/)
// Extract involved entities - full extraction, no limit
const entitySection = text.match(/### [Involved Entities]\n([\s\S]*?)(?=\n###|$)/)
if (entitySection) {
const lines = entitySection[1].split('\n').filter(l => l.trim().startsWith('-'))
result.entities = lines.map(l => {
@ -698,25 +698,25 @@ const parseInterview = (text) => {
}
try {
// 访
const topicMatch = text.match(/\*\*采访主题:\*\*\s*(.+?)(?:\n|$)/)
// Extract interview topic
const topicMatch = text.match(/\*\*Interview Topic:\*\*\s*(.+?)(?:\n|$)/)
if (topicMatch) result.topic = topicMatch[1].trim()
// 访 "5 / 9 Agent"
const countMatch = text.match(/\*\*采访人数:\*\*\s*(\d+)\s*\/\s*(\d+)/)
// Extract interview count (e.g. "5 / 9 simulated Agents")
const countMatch = text.match(/\*\*Interview Count:\*\*\s*(\d+)\s*\/\s*(\d+)/)
if (countMatch) {
result.successCount = parseInt(countMatch[1])
result.totalCount = parseInt(countMatch[2])
result.agentCount = `${countMatch[1]} / ${countMatch[2]}`
}
// 访
const reasonMatch = text.match(/### 采访对象选择理由\n([\s\S]*?)(?=\n---\n|\n### 采访实录)/)
// Extract interviewee selection rationale
const reasonMatch = text.match(/### Interviewee Selection Rationale\n([\s\S]*?)(?=\n---\n|\n### Interview Transcript)/)
if (reasonMatch) {
result.selectionReason = reasonMatch[1].trim()
}
//
// Parse each person's selection rationale
const parseIndividualReasons = (reasonText) => {
const reasons = {}
if (!reasonText) return reasons
@ -730,26 +730,26 @@ const parseInterview = (text) => {
let name = null
let reasonStart = null
// 1: . **index=X**
// : 1. **_345index=1**...
// Format 1: Number. **Name (index=X)**: rationale
// e.g.: 1. **Alumni_345 (index=1)**: As a Wuhan University alumni...
headerMatch = line.match(/^\d+\.\s*\*\*([^*(]+)(?:[(]index\s*=?\s*\d+[)])?\*\*[:]\s*(.*)/)
if (headerMatch) {
name = headerMatch[1].trim()
reasonStart = headerMatch[2]
}
// 2: - index X
// : - _601index 0...
// Format 2: - Select Name (index X): rationale
// e.g.: - Select Parent_601 (index 0): As a parent group representative...
if (!headerMatch) {
headerMatch = line.match(/^-\s*选择([^(]+)(?:[(]index\s*=?\s*\d+[)])?[:]\s*(.*)/)
headerMatch = line.match(/^-\s*Select([^(]+)(?:[(]index\s*=?\s*\d+[)])?[:]\s*(.*)/)
if (headerMatch) {
name = headerMatch[1].trim()
reasonStart = headerMatch[2]
}
}
// 3: - **index X**
// : - **_601index 0**...
// Format 3: - **Name (index X)**: rationale
// e.g.: - **Parent_601 (index 0)**: As a parent group representative...
if (!headerMatch) {
headerMatch = line.match(/^-\s*\*\*([^*(]+)(?:[(]index\s*=?\s*\d+[)])?\*\*[:]\s*(.*)/)
if (headerMatch) {
@ -759,20 +759,20 @@ const parseInterview = (text) => {
}
if (name) {
//
// Save previous person's rationale
if (currentName && currentReason.length > 0) {
reasons[currentName] = currentReason.join(' ').trim()
}
//
// Start new person
currentName = name
currentReason = reasonStart ? [reasonStart.trim()] : []
} else if (currentName && line.trim() && !line.match(/^未选|^综上|^最终选择/)) {
//
} else if (currentName && line.trim() && !line.match(/^Not selected|^In summary|^Final selection/)) {
// Continuation of rationale (exclude closing summary paragraph)
currentReason.push(line.trim())
}
}
//
// Save last person's rationale
if (currentName && currentReason.length > 0) {
reasons[currentName] = currentReason.join(' ').trim()
}
@ -782,8 +782,8 @@ const parseInterview = (text) => {
const individualReasons = parseIndividualReasons(result.selectionReason)
// 访
const interviewBlocks = text.split(/#### 采访 #\d+:/).slice(1)
// Extract each interview record
const interviewBlocks = text.split(/#### Interview #\d+:/).slice(1)
interviewBlocks.forEach((block, index) => {
const interview = {
@ -799,33 +799,33 @@ const parseInterview = (text) => {
quotes: []
}
// """"
// Extract title (e.g. "Student", "Education Professional", etc.)
const titleMatch = block.match(/^(.+?)\n/)
if (titleMatch) interview.title = titleMatch[1].trim()
//
// Extract name and role
const nameRoleMatch = block.match(/\*\*(.+?)\*\*\s*\((.+?)\)/)
if (nameRoleMatch) {
interview.name = nameRoleMatch[1].trim()
interview.role = nameRoleMatch[2].trim()
//
// Set this person's selection rationale
interview.selectionReason = individualReasons[interview.name] || ''
}
//
const bioMatch = block.match(/_简介:\s*([\s\S]*?)_\n/)
// Extract bio
const bioMatch = block.match(/_Bio:\s*([\s\S]*?)_\n/)
if (bioMatch) {
interview.bio = bioMatch[1].trim().replace(/\.\.\.$/, '...')
}
//
// Extract question list
const qMatch = block.match(/\*\*Q:\*\*\s*([\s\S]*?)(?=\n\n\*\*A:\*\*|\*\*A:\*\*)/)
if (qMatch) {
const qText = qMatch[1].trim()
//
// Split questions by number
const questions = qText.split(/\n\d+\.\s+/).filter(q => q.trim())
if (questions.length > 0) {
// "1."
// If first question has "1." prefix, handle specially
const firstQ = qText.match(/^1\.\s+(.+)/)
if (firstQ) {
interview.questions = [firstQ[1].trim(), ...questions.slice(1).map(q => q.trim())]
@ -835,14 +835,14 @@ const parseInterview = (text) => {
}
}
// - TwitterReddit
const answerMatch = block.match(/\*\*A:\*\*\s*([\s\S]*?)(?=\*\*关键引言|$)/)
// Extract answers - separate Twitter and Reddit
const answerMatch = block.match(/\*\*A:\*\*\s*([\s\S]*?)(?=\*\*Key Quotes|$)/)
if (answerMatch) {
const answerText = answerMatch[1].trim()
// TwitterReddit
const twitterMatch = answerText.match(/【Twitter平台回答】\n?([\s\S]*?)(?=【Reddit平台回答】|$)/)
const redditMatch = answerText.match(/【Reddit平台回答】\n?([\s\S]*?)$/)
// Separate Twitter and Reddit answers
const twitterMatch = answerText.match(/[Twitter Platform Answer]\n?([\s\S]*?)(?=[Reddit Platform Answer]|$)/)
const redditMatch = answerText.match(/[Reddit Platform Answer]\n?([\s\S]*?)$/)
if (twitterMatch) {
interview.twitterAnswer = twitterMatch[1].trim()
@ -851,29 +851,29 @@ const parseInterview = (text) => {
interview.redditAnswer = redditMatch[1].trim()
}
// 退
// Platform fallback logic (backward compat: single platform marker)
if (!twitterMatch && redditMatch) {
// Reddit
if (interview.redditAnswer && interview.redditAnswer !== '(该平台未获得回复)') {
// Only Reddit answer, copy as default display only if not placeholder text
if (interview.redditAnswer && interview.redditAnswer !== '(No response from this platform)') {
interview.twitterAnswer = interview.redditAnswer
}
} else if (twitterMatch && !redditMatch) {
if (interview.twitterAnswer && interview.twitterAnswer !== '(该平台未获得回复)') {
if (interview.twitterAnswer && interview.twitterAnswer !== '(No response from this platform)') {
interview.redditAnswer = interview.twitterAnswer
}
} else if (!twitterMatch && !redditMatch) {
//
// No platform markers (very old format), use entire text as answer
interview.twitterAnswer = answerText
}
}
//
const quotesMatch = block.match(/\*\*关键引言:\*\*\n([\s\S]*?)(?=\n---|\n####|$)/)
// Extract key quotes (compatible with multiple quote formats)
const quotesMatch = block.match(/\*\*Key Quotes:\*\*\n([\s\S]*?)(?=\n---|\n####|$)/)
if (quotesMatch) {
const quotesText = quotesMatch[1]
// > "text"
// Prefer > "text" format
let quoteMatches = quotesText.match(/> "([^"]+)"/g)
// 退 > "text" > \u201Ctext\u201D
// Fallback: match > "text" or > \u201Ctext\u201D (Chinese quotes)
if (!quoteMatches) {
quoteMatches = quotesText.match(/> [\u201C""]([^\u201D""]+)[\u201D""]/g)
}
@ -889,8 +889,8 @@ const parseInterview = (text) => {
}
})
// 访
const summaryMatch = text.match(/### 采访摘要与核心观点\n([\s\S]*?)$/)
// Extract interview summary
const summaryMatch = text.match(/### Interview Summary & Key Insights\n([\s\S]*?)$/)
if (summaryMatch) {
result.summary = summaryMatch[1].trim()
}
@ -911,23 +911,23 @@ const parseQuickSearch = (text) => {
}
try {
//
const queryMatch = text.match(/搜索查询:\s*(.+?)(?:\n|$)/)
// Extract search query
const queryMatch = text.match(/Search Query:\s*(.+?)(?:\n|$)/)
if (queryMatch) result.query = queryMatch[1].trim()
//
const countMatch = text.match(/找到\s*(\d+)\s*条/)
// Extract result count
const countMatch = text.match(/Found\s*(\d+)\s*results/)
if (countMatch) result.count = parseInt(countMatch[1])
// -
const factsSection = text.match(/### 相关事实:\n([\s\S]*)$/)
// Extract related facts - full extraction, no limit
const factsSection = text.match(/### Related Facts:\n([\s\S]*)$/)
if (factsSection) {
const lines = factsSection[1].split('\n').filter(l => l.match(/^\d+\./))
result.facts = lines.map(l => l.replace(/^\d+\.\s*/, '').trim()).filter(Boolean)
}
//
const edgesSection = text.match(/### 相关边:\n([\s\S]*?)(?=\n###|$)/)
// Try extracting edge info (if present)
const edgesSection = text.match(/### Related Edges:\n([\s\S]*?)(?=\n###|$)/)
if (edgesSection) {
const lines = edgesSection[1].split('\n').filter(l => l.trim().startsWith('-'))
result.edges = lines.map(l => {
@ -939,8 +939,8 @@ const parseQuickSearch = (text) => {
}).filter(Boolean)
}
//
const nodesSection = text.match(/### 相关节点:\n([\s\S]*?)(?=\n###|$)/)
// Try extracting node info (if present)
const nodesSection = text.match(/### Related Nodes:\n([\s\S]*?)(?=\n###|$)/)
if (nodesSection) {
const lines = nodesSection[1].split('\n').filter(l => l.trim().startsWith('-'))
result.nodes = lines.map(l => {
@ -1229,7 +1229,7 @@ const PanoramaDisplay = {
h('div', { class: 'fact-item historical', key: i }, [
h('span', { class: 'fact-number' }, i + 1),
h('div', { class: 'fact-content' }, [
// [time - time]
// Try extracting time info [time - time]
(() => {
const timeMatch = fact.match(/^\[(.+?)\]\s*(.*)$/)
if (timeMatch) {
@ -1296,16 +1296,16 @@ const InterviewDisplay = {
const activeIndex = ref(0)
const expandedAnswers = ref(new Set())
// -
// Maintain independent platform selection state per Q&A pair
const platformTabs = reactive({}) // { 'agentIdx-qIdx': 'twitter' | 'reddit' }
//
// Get current platform selection for a question
const getPlatformTab = (agentIdx, qIdx) => {
const key = `${agentIdx}-${qIdx}`
return platformTabs[key] || 'twitter'
}
//
// Set platform selection for a question
const setPlatformTab = (agentIdx, qIdx, platform) => {
const key = `${agentIdx}-${qIdx}`
platformTabs[key] = platform
@ -1327,26 +1327,26 @@ const InterviewDisplay = {
return text.substring(0, 400) + '...'
}
//
// Check if platform placeholder text
const isPlaceholderText = (text) => {
if (!text) return true
const t = text.trim()
return t === '(该平台未获得回复)' || t === '(该平台未获得回复)' || t === '[无回复]'
return t === '(No response from this platform)' || t === '(No response from this platform)' || t === '[No response]'
}
//
// Try splitting answer by question number
const splitAnswerByQuestions = (answerText, questionCount) => {
if (!answerText || questionCount <= 0) return [answerText]
if (isPlaceholderText(answerText)) return ['']
//
// 1. "X" "X:"
// 2. "1. " "\n1. " +
// Support two numbering formats:
// 1. "Question X:" or "Question X:" (Chinese format, new backend format)
// 2. "1. " or "\n1. " (number + dot, old format compat)
let matches = []
let match
// "X"
const cnPattern = /(?:^|[\r\n]+)问题(\d+)[:]\s*/g
// Prefer "Question X:" format
const cnPattern = /(?:^|[\r\n]+)Question(\d+)[:]\s*/g
while ((match = cnPattern.exec(answerText)) !== null) {
matches.push({
num: parseInt(match[1]),
@ -1355,7 +1355,7 @@ const InterviewDisplay = {
})
}
// 退 "."
// If no match, fall back to "number." format
if (matches.length === 0) {
const numPattern = /(?:^|[\r\n]+)(\d+)\.\s+/g
while ((match = numPattern.exec(answerText)) !== null) {
@ -1367,16 +1367,16 @@ const InterviewDisplay = {
}
}
//
// If no numbering found or only one found, return whole
if (matches.length <= 1) {
const cleaned = answerText
.replace(/^问题\d+[:]\s*/, '')
.replace(/^Question\d+[:]\s*/, '')
.replace(/^\d+\.\s+/, '')
.trim()
return [cleaned || answerText]
}
//
// Extract parts by number
const parts = []
for (let i = 0; i < matches.length; i++) {
const current = matches[i]
@ -1397,7 +1397,7 @@ const InterviewDisplay = {
return [answerText]
}
//
// Get answer for a specific question
const getAnswerForQuestion = (interview, qIdx, platform) => {
const answer = platform === 'twitter' ? interview.twitterAnswer : (interview.redditAnswer || interview.twitterAnswer)
if (!answer || isPlaceholderText(answer)) return answer || ''
@ -1405,21 +1405,21 @@ const InterviewDisplay = {
const questionCount = interview.questions?.length || 1
const answers = splitAnswerByQuestions(answer, questionCount)
//
// Split succeeded and index valid
if (answers.length > 1 && qIdx < answers.length) {
return answers[qIdx] || ''
}
//
// Split failed: first question returns full answer, rest return empty
return qIdx === 0 ? answer : ''
}
//
// Check if a question has dual-platform answers (filter placeholders)
const hasMultiplePlatforms = (interview, qIdx) => {
if (!interview.twitterAnswer || !interview.redditAnswer) return false
const twitterAnswer = getAnswerForQuestion(interview, qIdx, 'twitter')
const redditAnswer = getAnswerForQuestion(interview, qIdx, 'reddit')
//
// Both platforms have real answers (non-placeholder) and content differs
return !isPlaceholderText(twitterAnswer) && !isPlaceholderText(redditAnswer) && twitterAnswer !== redditAnswer
}
@ -1469,13 +1469,13 @@ const InterviewDisplay = {
])
]),
// Selection Reason -
// Selection Reason
props.result.interviews[activeIndex.value]?.selectionReason && h('div', { class: 'selection-reason' }, [
h('div', { class: 'reason-label' }, '选择理由'),
h('div', { class: 'reason-label' }, 'Selection Reason'),
h('div', { class: 'reason-content' }, props.result.interviews[activeIndex.value].selectionReason)
]),
// Q&A Conversation Thread -
// Q&A Conversation Thread - Q&A style
h('div', { class: 'qa-thread' },
(props.result.interviews[activeIndex.value]?.questions?.length > 0
? props.result.interviews[activeIndex.value].questions
@ -1505,7 +1505,7 @@ const InterviewDisplay = {
h('div', { class: 'qa-content' }, [
h('div', { class: 'qa-answer-header' }, [
h('div', { class: 'qa-sender' }, interview?.name || 'Agent'),
//
// Dual-platform toggle button (only shown when real dual-platform answers exist)
hasDualPlatform && h('div', { class: 'platform-switch' }, [
h('button', {
class: ['platform-btn', { active: currentPlatform === 'twitter' }],
@ -1537,7 +1537,7 @@ const InterviewDisplay = {
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\n/g, '<br>')
}),
// Expand/Collapse Button
// Expand/Collapse Button (not shown for placeholder text)
!isPlaceholder && answerText.length > 400 && h('button', {
class: 'expand-answer-btn',
onClick: () => toggleAnswer(expandKey)
@ -1769,19 +1769,19 @@ const isFinalizing = computed(() => {
return !isComplete.value && isPlanningDone.value && totalSections.value > 0 && completedSections.value >= totalSections.value
})
//
// Currently active step (for top display)
const activeStep = computed(() => {
const steps = workflowSteps.value
// active
// Find currently active step
const active = steps.find(s => s.status === 'active')
if (active) return active
// active done
// If no active, return last done step
const doneSteps = steps.filter(s => s.status === 'done')
if (doneSteps.length > 0) return doneSteps[doneSteps.length - 1]
//
return steps[0] || { noLabel: '--', title: '等待开始', status: 'todo', meta: '' }
// Otherwise return first step
return steps[0] || { noLabel: '--', title: 'Waiting to start', status: 'todo', meta: '' }
})
const workflowSteps = computed(() => {
@ -1874,25 +1874,25 @@ const truncateText = (text, maxLen) => {
const renderMarkdown = (content) => {
if (!content) return ''
// ## xxx
// Remove leading level-2 headings (## xxx), as section titles are shown in outer layer
let processedContent = content.replace(/^##\s+.+\n+/, '')
//
// Process code blocks
let html = processedContent.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre class="code-block"><code>$2</code></pre>')
//
// Process inline code
html = html.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>')
//
// Process headings
html = html.replace(/^#### (.+)$/gm, '<h5 class="md-h5">$1</h5>')
html = html.replace(/^### (.+)$/gm, '<h4 class="md-h4">$1</h4>')
html = html.replace(/^## (.+)$/gm, '<h3 class="md-h3">$1</h3>')
html = html.replace(/^# (.+)$/gm, '<h2 class="md-h2">$1</h2>')
//
// Process blockquotes
html = html.replace(/^> (.+)$/gm, '<blockquote class="md-quote">$1</blockquote>')
// -
// Process lists - support sub-lists
html = html.replace(/^(\s*)- (.+)$/gm, (match, indent, text) => {
const level = Math.floor(indent.length / 2)
return `<li class="md-li" data-level="${level}">${text}</li>`
@ -1902,52 +1902,52 @@ const renderMarkdown = (content) => {
return `<li class="md-oli" data-level="${level}">${text}</li>`
})
//
// Wrap unordered list
html = html.replace(/(<li class="md-li"[^>]*>.*?<\/li>\s*)+/g, '<ul class="md-ul">$&</ul>')
//
// Wrap ordered list
html = html.replace(/(<li class="md-oli"[^>]*>.*?<\/li>\s*)+/g, '<ol class="md-ol">$&</ol>')
//
// Clean all whitespace between list items
html = html.replace(/<\/li>\s+<li/g, '</li><li')
//
// Clean whitespace after list start tag
html = html.replace(/<ul class="md-ul">\s+/g, '<ul class="md-ul">')
html = html.replace(/<ol class="md-ol">\s+/g, '<ol class="md-ol">')
//
// Clean whitespace before list end tag
html = html.replace(/\s+<\/ul>/g, '</ul>')
html = html.replace(/\s+<\/ol>/g, '</ol>')
//
// Process bold and italic
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>')
html = html.replace(/_(.+?)_/g, '<em>$1</em>')
// 线
// Process horizontal rules
html = html.replace(/^---$/gm, '<hr class="md-hr">')
// - <br>
// Process line breaks - blank lines become paragraph separators, single line breaks become <br>
html = html.replace(/\n\n/g, '</p><p class="md-p">')
html = html.replace(/\n/g, '<br>')
//
// Wrap in paragraphs
html = '<p class="md-p">' + html + '</p>'
//
// Clean empty paragraphs
html = html.replace(/<p class="md-p"><\/p>/g, '')
html = html.replace(/<p class="md-p">(<h[2-5])/g, '$1')
html = html.replace(/(<\/h[2-5]>)<\/p>/g, '$1')
html = html.replace(/<p class="md-p">(<ul|<ol|<blockquote|<pre|<hr)/g, '$1')
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>|<\/pre>)<\/p>/g, '$1')
// <br>
// Clean <br> tags around block-level elements
html = html.replace(/<br>\s*(<ul|<ol|<blockquote)/g, '$1')
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>)\s*<br>/g, '$1')
// <p><br>
// Clean <p><br> immediately following block-level elements (caused by extra blank lines)
html = html.replace(/<p class="md-p">(<br>\s*)+(<ul|<ol|<blockquote|<pre|<hr)/g, '$2')
// <br>
// Clean consecutive <br> tags
html = html.replace(/(<br>\s*){2,}/g, '<br>')
// <br>
// Clean <br> before paragraph start tag following block-level element
html = html.replace(/(<\/ol>|<\/ul>|<\/blockquote>)<br>(<p|<div)/g, '$1$2')
// <ol>
// Fix non-consecutive ordered list numbering: when single-item <ol> is separated by paragraph content, keep numbering incrementing
const tokens = html.split(/(<ol class="md-ol">(?:<li class="md-oli"[^>]*>[\s\S]*?<\/li>)+<\/ol>)/g)
let olCounter = 0
let inSequence = false
@ -2011,9 +2011,9 @@ const getActionLabel = (action) => {
}
const getLogLevelClass = (log) => {
if (log.includes('ERROR') || log.includes('错误')) return 'error'
if (log.includes('WARNING') || log.includes('警告')) return 'warning'
// INFO 使 success
if (log.includes('ERROR') || log.includes('Error')) return 'error'
if (log.includes('WARNING') || log.includes('Warning')) return 'warning'
// INFO uses default color, not marked as success
return ''
}
@ -2042,11 +2042,11 @@ const fetchAgentLog = async () => {
currentSectionIndex.value = log.section_index
}
// section_complete -
// section_complete - section generation complete
if (log.action === 'section_complete') {
if (log.details?.content) {
generatedSections.value[log.section_index] = log.details.content
//
// Auto-expand newly generated section
expandedContent.value.add(log.section_index - 1)
currentSectionIndex.value = null
}
@ -2054,10 +2054,10 @@ const fetchAgentLog = async () => {
if (log.action === 'report_complete') {
isComplete.value = true
currentSectionIndex.value = null // loading
currentSectionIndex.value = null // Ensure loading state is cleared
emit('update-status', 'completed')
stopPolling()
// nextTick
// Scroll logic handled uniformly in nextTick after loop ends
}
if (log.action === 'report_start') {
@ -2069,7 +2069,7 @@ const fetchAgentLog = async () => {
nextTick(() => {
if (rightPanel.value) {
//
// If task completed, scroll to top; otherwise scroll to bottom to follow latest logs
if (isComplete.value) {
rightPanel.value.scrollTop = 0
} else {
@ -2084,39 +2084,39 @@ const fetchAgentLog = async () => {
}
}
// - LLM response
// Extract final answer content - extract section content from LLM response
const extractFinalContent = (response) => {
if (!response) return null
// <final_answer>
// Try extracting content inside <final_answer> tags
const finalAnswerTagMatch = response.match(/<final_answer>([\s\S]*?)<\/final_answer>/)
if (finalAnswerTagMatch) {
return finalAnswerTagMatch[1].trim()
}
// Final Answer:
// 1: Final Answer:\n\n
// 2: Final Answer:
// Try finding content after Final Answer: (supports multiple formats)
// Format 1: Final Answer:\n\ncontent
// Format 2: Final Answer: content
const finalAnswerMatch = response.match(/Final\s*Answer:\s*\n*([\s\S]*)$/i)
if (finalAnswerMatch) {
return finalAnswerMatch[1].trim()
}
// :
const chineseFinalMatch = response.match(/最终答案[:]\s*\n*([\s\S]*)$/i)
// Try finding content after Final Answer: (Chinese)
const chineseFinalMatch = response.match(/Final Answer[:]\s*\n*([\s\S]*)$/i)
if (chineseFinalMatch) {
return chineseFinalMatch[1].trim()
}
// ## # > markdown
// If starts with ## or # or >, may be direct markdown content
const trimmedResponse = response.trim()
if (trimmedResponse.match(/^[#>]/)) {
return trimmedResponse
}
// markdown
// If content is long and contains markdown, try removing thinking process and return
if (response.length > 300 && (response.includes('**') || response.includes('>'))) {
// Thought:
// Remove thinking process starting with Thought:
const thoughtMatch = response.match(/^Thought:[\s\S]*?(?=\n\n[^T]|\n\n$)/i)
if (thoughtMatch) {
const afterThought = response.substring(thoughtMatch[0].length).trim()
@ -2461,7 +2461,7 @@ watch(() => props.reportId, (newId) => {
.section-number {
font-family: 'JetBrains Mono', monospace;
font-size: 16px;
color: #9CA3AF; /* 深灰色,不随状态变化 */
color: #9CA3AF; /* Dark gray, doesn't change with status */
font-weight: 500;
}
@ -3903,7 +3903,7 @@ watch(() => props.reportId, (newId) => {
overflow: hidden;
}
/* Selection Reason - 选择理由 */
/* Selection Reason */
:deep(.interview-display .selection-reason) {
background: #F8FAFC;
border: 1px solid #E2E8F0;
@ -5102,7 +5102,7 @@ watch(() => props.reportId, (newId) => {
border-radius: 4px;
}
/* Console Logs - 与 Step3Simulation.vue 保持一致 */
/* Console Logs - consistent with Step3Simulation.vue */
.console-logs {
background: #000;
color: #DDD;

View File

@ -437,7 +437,7 @@ const showToolsDetail = ref(true)
// Chat State
const chatInput = ref('')
const chatHistory = ref([])
const chatHistoryCache = ref({}) // : { 'report_agent': [], 'agent_0': [], 'agent_1': [], ... }
const chatHistoryCache = ref({}) // Cache all chat records: { 'report_agent': [], 'agent_0': [], 'agent_1': [], ... }
const isSending = ref(false)
const chatMessages = ref(null)
const chatInputRef = ref(null)
@ -487,7 +487,7 @@ const selectChatTarget = (target) => {
}
}
//
// Save current chat records to cache
const saveChatHistory = () => {
if (chatHistory.value.length === 0) return
@ -499,7 +499,7 @@ const saveChatHistory = () => {
}
const selectReportAgentChat = () => {
//
// Save current chat records
saveChatHistory()
activeTab.value = 'chat'
@ -508,7 +508,7 @@ const selectReportAgentChat = () => {
selectedAgentIndex.value = null
showAgentDropdown.value = false
// Report Agent
// Restore Report Agent chat records
chatHistory.value = chatHistoryCache.value['report_agent'] || []
}
@ -528,7 +528,7 @@ const toggleAgentDropdown = () => {
}
const selectAgent = (agent, idx) => {
//
// Save current chat records
saveChatHistory()
selectedAgent.value = agent
@ -536,7 +536,7 @@ const selectAgent = (agent, idx) => {
chatTarget.value = 'agent'
showAgentDropdown.value = false
// Agent
// Restore this Agent's chat records
chatHistory.value = chatHistoryCache.value[`agent_${idx}`] || []
addLog(t('log.selectChatTarget', { name: agent.username }))
}
@ -566,7 +566,7 @@ const renderMarkdown = (content) => {
html = html.replace(/^# (.+)$/gm, '<h2 class="md-h2">$1</h2>')
html = html.replace(/^> (.+)$/gm, '<blockquote class="md-quote">$1</blockquote>')
// -
// Process lists - support sub-lists
html = html.replace(/^(\s*)- (.+)$/gm, (match, indent, text) => {
const level = Math.floor(indent.length / 2)
return `<li class="md-li" data-level="${level}">${text}</li>`
@ -576,17 +576,17 @@ const renderMarkdown = (content) => {
return `<li class="md-oli" data-level="${level}">${text}</li>`
})
//
// Wrap unordered list
html = html.replace(/(<li class="md-li"[^>]*>.*?<\/li>\s*)+/g, '<ul class="md-ul">$&</ul>')
//
// Wrap ordered list
html = html.replace(/(<li class="md-oli"[^>]*>.*?<\/li>\s*)+/g, '<ol class="md-ol">$&</ol>')
//
// Clean all whitespace between list items
html = html.replace(/<\/li>\s+<li/g, '</li><li')
//
// Clean whitespace after list start tag
html = html.replace(/<ul class="md-ul">\s+/g, '<ul class="md-ul">')
html = html.replace(/<ol class="md-ol">\s+/g, '<ol class="md-ol">')
//
// Clean whitespace before list end tag
html = html.replace(/\s+<\/ul>/g, '</ul>')
html = html.replace(/\s+<\/ol>/g, '</ol>')
@ -602,17 +602,17 @@ const renderMarkdown = (content) => {
html = html.replace(/(<\/h[2-5]>)<\/p>/g, '$1')
html = html.replace(/<p class="md-p">(<ul|<ol|<blockquote|<pre|<hr)/g, '$1')
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>|<\/pre>)<\/p>/g, '$1')
// <br>
// Clean <br> tags around block-level elements
html = html.replace(/<br>\s*(<ul|<ol|<blockquote)/g, '$1')
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>)\s*<br>/g, '$1')
// <p><br>
// Clean <p><br> immediately following block-level elements (caused by extra blank lines)
html = html.replace(/<p class="md-p">(<br>\s*)+(<ul|<ol|<blockquote|<pre|<hr)/g, '$2')
// <br>
// Clean consecutive <br> tags
html = html.replace(/(<br>\s*){2,}/g, '<br>')
// <br>
// Clean <br> before paragraph start tag following block-level element
html = html.replace(/(<\/ol>|<\/ul>|<\/blockquote>)<br>(<p|<div)/g, '$1$2')
// <ol>
// Fix non-consecutive ordered list numbering: when single-item <ol> is separated by paragraph content, keep numbering incrementing
const tokens = html.split(/(<ol class="md-ol">(?:<li class="md-oli"[^>]*>[\s\S]*?<\/li>)+<\/ol>)/g)
let olCounter = 0
let inSequence = false
@ -674,7 +674,7 @@ const sendMessage = async () => {
} finally {
isSending.value = false
scrollToBottom()
//
// Auto-save chat records to cache
saveChatHistory()
}
}
@ -722,9 +722,9 @@ const sendToAgent = async (message) => {
const historyContext = chatHistory.value
.slice(0, -1)
.slice(-6)
.map(msg => `${msg.role === 'user' ? '提问者' : '你'}${msg.content}`)
.map(msg => `${msg.role === 'user' ? 'Questioner' : 'You'}: ${msg.content}`)
.join('\n')
prompt = `以下是我们之前的对话:\n${historyContext}\n\n现在我的新问题是${message}`
prompt = `The following is our previous conversation:\n${historyContext}\n\nMy new question now is: ${message}`
}
const res = await interviewAgents({
@ -736,17 +736,17 @@ const sendToAgent = async (message) => {
})
if (res.success && res.data) {
// : res.data.result.results
// : {"twitter_0": {...}, "reddit_0": {...}} {"reddit_0": {...}}
// Correct data path: res.data.result.results is an object dictionary
// Format: {"twitter_0": {...}, "reddit_0": {...}} or single platform {"reddit_0": {...}}
const resultData = res.data.result || res.data
const resultsDict = resultData.results || resultData
// reddit
// Convert object dictionary to array, prefer reddit platform responses
let responseContent = null
const agentId = selectedAgentIndex.value
if (typeof resultsDict === 'object' && !Array.isArray(resultsDict)) {
// 使 reddit twitter
// Prefer reddit platform response, then twitter
const redditKey = `reddit_${agentId}`
const twitterKey = `twitter_${agentId}`
const agentResult = resultsDict[redditKey] || resultsDict[twitterKey] || Object.values(resultsDict)[0]
@ -754,7 +754,7 @@ const sendToAgent = async (message) => {
responseContent = agentResult.response || agentResult.answer
}
} else if (Array.isArray(resultsDict) && resultsDict.length > 0) {
//
// Compatible with array format
responseContent = resultsDict[0].response || resultsDict[0].answer
}
@ -820,19 +820,19 @@ const submitSurvey = async () => {
})
if (res.success && res.data) {
// : res.data.result.results
// : {"twitter_0": {...}, "reddit_0": {...}, "twitter_1": {...}, ...}
// Correct data path: res.data.result.results is an object dictionary
// Format: {"twitter_0": {...}, "reddit_0": {...}, "twitter_1": {...}, ...}
const resultData = res.data.result || res.data
const resultsDict = resultData.results || resultData
//
// Convert object dictionary to array format
const surveyResultsList = []
for (const interview of interviews) {
const agentIdx = interview.agent_id
const agent = profiles.value[agentIdx]
// 使 reddit twitter
// Prefer reddit platform response, then twitter
let responseContent = t('step5.noResponse')
if (typeof resultsDict === 'object' && !Array.isArray(resultsDict)) {
@ -843,7 +843,7 @@ const submitSurvey = async () => {
responseContent = agentResult.response || agentResult.answer || t('step5.noResponse')
}
} else if (Array.isArray(resultsDict)) {
//
// Compatible with array format
const matchedResult = resultsDict.find(r => r.agent_id === agentIdx)
if (matchedResult) {
responseContent = matchedResult.response || matchedResult.answer || t('step5.noResponse')
@ -983,7 +983,7 @@ watch(() => props.simulationId, (newId) => {
overflow: hidden;
}
/* Left Panel - Report Style (与 Step4Report.vue 完全一致) */
/* Left Panel - Report Style (identical to Step4Report.vue) */
.left-panel.report-style {
width: 45%;
min-width: 450px;
@ -2031,7 +2031,7 @@ watch(() => props.simulationId, (newId) => {
margin-bottom: 0;
}
/* 修复有序列表编号 - 使用 CSS 计数器让多个 ol 连续编号 */
/* Fix ordered list numbering - use CSS counters for consecutive numbering across multiple ol */
.message-text {
counter-reset: list-counter;
}
@ -2057,7 +2057,7 @@ watch(() => props.simulationId, (newId) => {
flex-shrink: 0;
}
/* 无序列表样式 */
/* Unordered list styles */
.message-text :deep(.md-ul) {
padding-left: 20px;
margin: 8px 0;
@ -2536,7 +2536,7 @@ watch(() => props.simulationId, (newId) => {
margin: 6px 0;
}
/* 聊天/问卷区域的引用样式 */
/* Chat/Survey area quote styles */
.chat-messages :deep(.md-quote),
.result-answer :deep(.md-quote) {
margin: 12px 0;

View File

@ -14,12 +14,12 @@ for (const path in localeFiles) {
}
}
const savedLocale = localStorage.getItem('locale') || 'zh'
const savedLocale = localStorage.getItem('locale') || 'en'
const i18n = createI18n({
legacy: false,
locale: savedLocale,
fallbackLocale: 'zh',
fallbackLocale: 'en',
messages
})

View File

@ -1,6 +1,6 @@
/**
* 临时存储待上传的文件和需求
* 用于首页点击启动引擎后立即跳转在Process页面再进行API调用
* Temporarily store pending upload files and requirements
* Used to jump immediately after clicking start engine on home page, then make API calls on Process page
*/
import { reactive } from 'vue'

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@
/>
</div>
<!-- Right Panel: Step5 深度互动 -->
<!-- Right Panel: Step5 Deep Interaction -->
<div class="panel-wrapper right" :style="rightPanelStyle">
<Step5Interaction
:reportId="currentReportId"
@ -83,7 +83,7 @@ const props = defineProps({
reportId: String
})
// Layout State -
// Layout State - default to workbench view
const viewMode = ref('workbench')
// Data State
@ -147,26 +147,26 @@ const loadReportData = async () => {
try {
addLog(t('log.loadReportData', { id: currentReportId.value }))
// report simulation_id
// Get report info to obtain simulation_id
const reportRes = await getReport(currentReportId.value)
if (reportRes.success && reportRes.data) {
const reportData = reportRes.data
simulationId.value = reportData.simulation_id
if (simulationId.value) {
// simulation
// Get simulation info
const simRes = await getSimulation(simulationId.value)
if (simRes.success && simRes.data) {
const simData = simRes.data
// project
// Get project info
if (simData.project_id) {
const projRes = await getProject(simData.project_id)
if (projRes.success && projRes.data) {
projectData.value = projRes.data
addLog(t('log.projectLoadSuccess', { id: projRes.data.project_id }))
// graph
// Get graph data
if (projRes.data.graph_id) {
await loadGraph(projRes.data.graph_id)
}

View File

@ -50,7 +50,7 @@
<!-- Right Panel: Step Components -->
<div class="panel-wrapper right" :style="rightPanelStyle">
<!-- Step 1: 图谱构建 -->
<!-- Step 1: Graph Build -->
<Step1GraphBuild
v-if="currentStep === 1"
:currentPhase="currentPhase"
@ -61,7 +61,7 @@
:systemLogs="systemLogs"
@next-step="handleNextStep"
/>
<!-- Step 2: 环境搭建 -->
<!-- Step 2: Environment Setup -->
<Step2EnvSetup
v-else-if="currentStep === 2"
:projectData="projectData"
@ -95,7 +95,7 @@ const { t, tm } = useI18n()
const viewMode = ref('split') // graph | split | workbench
// Step State
const currentStep = ref(1) // 1: , 2: , 3: , 4: , 5:
const currentStep = ref(1) // 1: Graph Build, 2: Env Setup, 3: Run Simulation, 4: Report Generation, 5: Deep Interaction
const stepNames = computed(() => tm('main.stepNames'))
// Data State
@ -166,7 +166,7 @@ const handleNextStep = (params = {}) => {
currentStep.value++
addLog(t('log.enterStep', { step: currentStep.value, name: stepNames.value[currentStep.value - 1] }))
// Step 2 Step 3
// If entering Step 3 from Step 2, record simulation rounds config
if (currentStep.value === 3 && params.maxRounds) {
addLog(t('log.customSimRounds', { rounds: params.maxRounds }))
}

File diff suppressed because it is too large Load Diff

View File

@ -49,7 +49,7 @@
/>
</div>
<!-- Right Panel: Step4 报告生成 -->
<!-- Right Panel: Step4 Report Generation -->
<div class="panel-wrapper right" :style="rightPanelStyle">
<Step4Report
:reportId="currentReportId"
@ -83,7 +83,7 @@ const props = defineProps({
reportId: String
})
// Layout State -
// Layout State - default to workbench view
const viewMode = ref('workbench')
// Data State
@ -146,26 +146,26 @@ const loadReportData = async () => {
try {
addLog(t('log.loadReportData', { id: currentReportId.value }))
// report simulation_id
// Get report info to obtain simulation_id
const reportRes = await getReport(currentReportId.value)
if (reportRes.success && reportRes.data) {
const reportData = reportRes.data
simulationId.value = reportData.simulation_id
if (simulationId.value) {
// simulation
// Get simulation info
const simRes = await getSimulation(simulationId.value)
if (simRes.success && simRes.data) {
const simData = simRes.data
// project
// Get project info
if (simData.project_id) {
const projRes = await getProject(simData.project_id)
if (projRes.success && projRes.data) {
projectData.value = projRes.data
addLog(t('log.projectLoadSuccess', { id: projRes.data.project_id }))
// graph
// Get graph data
if (projRes.data.graph_id) {
await loadGraph(projRes.data.graph_id)
}

View File

@ -49,7 +49,7 @@
/>
</div>
<!-- Right Panel: Step3 开始模拟 -->
<!-- Right Panel: Step3 Run Simulation -->
<div class="panel-wrapper right" :style="rightPanelStyle">
<Step3Simulation
:simulationId="currentSimulationId"
@ -92,9 +92,9 @@ const viewMode = ref('split')
// Data State
const currentSimulationId = ref(route.params.simulationId)
// query maxRounds
// Get maxRounds from query params at init, ensuring child component gets value immediately
const maxRounds = ref(route.query.maxRounds ? parseInt(route.query.maxRounds) : null)
const minutesPerRound = ref(30) // 30
const minutesPerRound = ref(30) // Default: 30 minutes per round
const projectData = ref(null)
const graphData = ref(null)
const graphLoading = ref(false)
@ -150,14 +150,14 @@ const toggleMaximize = (target) => {
}
const handleGoBack = async () => {
// Step 2
// Before returning to Step 2, close running simulation first
addLog(t('log.preparingGoBack'))
//
// Stop polling
stopGraphRefresh()
try {
//
// Try graceful close of simulation environment first
const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value })
if (envStatusRes.success && envStatusRes.data?.env_alive) {
@ -178,7 +178,7 @@ const handleGoBack = async () => {
}
}
} else {
//
// Environment not running, check if process needs stopping
if (isSimulating.value) {
addLog(t('log.stoppingSimProcess'))
try {
@ -193,13 +193,13 @@ const handleGoBack = async () => {
addLog(t('log.checkStatusFailed', { error: err.message }))
}
// Step 2 ()
// Return to Step 2 (Environment Setup)
router.push({ name: 'Simulation', params: { simulationId: currentSimulationId.value } })
}
const handleNextStep = () => {
// Step3Simulation
//
// Step3Simulation component handles report generation and routing directly
// This method is only a fallback
addLog(t('log.enterStep4'))
}
@ -208,12 +208,12 @@ const loadSimulationData = async () => {
try {
addLog(t('log.loadingSimData', { id: currentSimulationId.value }))
// simulation
// Get simulation info
const simRes = await getSimulation(currentSimulationId.value)
if (simRes.success && simRes.data) {
const simData = simRes.data
// simulation config minutes_per_round
// Get simulation config to obtain minutes_per_round
try {
const configRes = await getSimulationConfig(currentSimulationId.value)
if (configRes.success && configRes.data?.time_config?.minutes_per_round) {
@ -224,14 +224,14 @@ const loadSimulationData = async () => {
addLog(t('log.timeConfigFetchFailed', { minutes: minutesPerRound.value }))
}
// project
// Get project info
if (simData.project_id) {
const projRes = await getProject(simData.project_id)
if (projRes.success && projRes.data) {
projectData.value = projRes.data
addLog(t('log.projectLoadSuccess', { id: projRes.data.project_id }))
// graph
// Get graph data
if (projRes.data.graph_id) {
await loadGraph(projRes.data.graph_id)
}
@ -246,8 +246,8 @@ const loadSimulationData = async () => {
}
const loadGraph = async (graphId) => {
// loading
// loading
// When simulation is running, auto-refresh doesn't show fullscreen loading to avoid flicker
// Show loading on manual refresh or initial load
if (!isSimulating.value) {
graphLoading.value = true
}
@ -279,7 +279,7 @@ let graphRefreshTimer = null
const startGraphRefresh = () => {
if (graphRefreshTimer) return
addLog(t('log.graphRealtimeRefreshStart'))
// 30
// Refresh immediately once, then every 30 seconds
graphRefreshTimer = setInterval(refreshGraph, 30000)
}
@ -302,7 +302,7 @@ watch(isSimulating, (newValue) => {
onMounted(() => {
addLog(t('log.simRunViewInit'))
// maxRounds query
// Record maxRounds config (value already obtained from query params at init)
if (maxRounds.value) {
addLog(t('log.customRounds', { rounds: maxRounds.value }))
}

View File

@ -48,7 +48,7 @@
/>
</div>
<!-- Right Panel: Step2 环境搭建 -->
<!-- Right Panel: Step2 Environment Setup -->
<div class="panel-wrapper right" :style="rightPanelStyle">
<Step2EnvSetup
:simulationId="currentSimulationId"
@ -142,7 +142,7 @@ const toggleMaximize = (target) => {
}
const handleGoBack = () => {
// process
// Return to process page
if (projectData.value?.project_id) {
router.push({ name: 'Process', params: { projectId: projectData.value.project_id } })
} else {
@ -153,65 +153,65 @@ const handleGoBack = () => {
const handleNextStep = (params = {}) => {
addLog(t('log.enterStep3'))
//
// Record simulation rounds config
if (params.maxRounds) {
addLog(t('log.customRoundsConfig', { rounds: params.maxRounds }))
} else {
addLog(t('log.useAutoRounds'))
}
//
// Build route params
const routeParams = {
name: 'SimulationRun',
params: { simulationId: currentSimulationId.value }
}
// query
// If custom rounds, pass via query params
if (params.maxRounds) {
routeParams.query = { maxRounds: params.maxRounds }
}
// Step 3
// Navigate to Step 3 page
router.push(routeParams)
}
// --- Data Logic ---
/**
* 检查并关闭正在运行的模拟
* 当用户从 Step 3 返回到 Step 2 默认用户要退出模拟
* Check and close running simulation
* When user returns from Step 3 to Step 2, assume user wants to exit simulation
*/
const checkAndStopRunningSimulation = async () => {
if (!currentSimulationId.value) return
try {
//
// First check if simulation environment is alive
const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value })
if (envStatusRes.success && envStatusRes.data?.env_alive) {
addLog(t('log.detectedSimEnvRunning'))
//
// Try graceful close of simulation environment
try {
const closeRes = await closeSimulationEnv({
simulation_id: currentSimulationId.value,
timeout: 10 // 10
timeout: 10 // 10 second timeout
})
if (closeRes.success) {
addLog(t('log.simEnvClosed'))
} else {
addLog(t('log.closeSimEnvFailedWithError', { error: closeRes.error || t('common.unknownError') }))
//
// If graceful close fails, try force stop
await forceStopSimulation()
}
} catch (closeErr) {
addLog(t('log.closeSimEnvException', { error: closeErr.message }))
//
// If graceful close throws, try force stop
await forceStopSimulation()
}
} else {
//
// Environment not running, but process may still exist, checking simulation status
const simRes = await getSimulation(currentSimulationId.value)
if (simRes.success && simRes.data?.status === 'running') {
addLog(t('log.detectedSimRunning'))
@ -219,13 +219,13 @@ const checkAndStopRunningSimulation = async () => {
}
}
} catch (err) {
//
console.warn('检查模拟状态失败:', err)
// Environment status check failure doesn't affect subsequent flow
console.warn('Failed to check simulation status:', err)
}
}
/**
* 强制停止模拟
* Force stop simulation
*/
const forceStopSimulation = async () => {
try {
@ -244,19 +244,19 @@ const loadSimulationData = async () => {
try {
addLog(t('log.loadingSimData', { id: currentSimulationId.value }))
// simulation
// Get simulation info
const simRes = await getSimulation(currentSimulationId.value)
if (simRes.success && simRes.data) {
const simData = simRes.data
// project
// Get project info
if (simData.project_id) {
const projRes = await getProject(simData.project_id)
if (projRes.success && projRes.data) {
projectData.value = projRes.data
addLog(t('log.projectLoadSuccess', { id: projRes.data.project_id }))
// graph
// Get graph data
if (projRes.data.graph_id) {
await loadGraph(projRes.data.graph_id)
}
@ -294,10 +294,10 @@ const refreshGraph = () => {
onMounted(async () => {
addLog(t('log.simViewInit'))
// Step 3
// Check and close running simulation (when user returns from Step 3)
await checkAndStopRunningSimulation()
//
// Load simulation data
loadSimulationData()
})
</script>

View File

@ -33,43 +33,45 @@
"visitGithub": "Visit our Github page"
},
"home": {
"tagline": "Concise & Universal Swarm Intelligence Engine",
"version": "/ v0.1-Preview",
"heroTitle1": "Upload Reports,",
"heroTitle2": "Predict the Future",
"heroDesc": "From a single document, {brand} extracts reality seeds to auto-generate a parallel world with up to {agentScale}. Inject variables from a god's-eye view to find the {optimalSolution} in complex group dynamics.",
"eyebrow": "Sentiment Intelligence Platform",
"version": "v0.1 · Preview",
"heroTitle1": "Know how the public",
"heroTitle2": "will respond",
"heroTitle3": "before you commit.",
"heroDesc": "{brand} ingests policy briefs and regulatory documents, then runs multi-agent simulations across social platforms to forecast how real stakeholder groups react over time. Decision-makers see the {optimalSolution} before a single line is published.",
"heroDescBrand": "MiroFish",
"heroDescAgentScale": "million-scale Agents",
"heroDescOptimalSolution": "\"local optimum\"",
"slogan": "Let Agents rehearse the future, let decisions prevail",
"systemStatus": "System Status",
"systemReady": "Ready",
"systemReadyDesc": "Prediction engine on standby. Upload unstructured data to initialize a simulation sequence.",
"metricLowCost": "Low Cost",
"metricLowCostDesc": "Avg. $5/sim",
"metricHighAvail": "Scalable",
"metricHighAvailDesc": "Millions of Agents",
"workflowSequence": "Workflow",
"step01Title": "Graph Build",
"step01Desc": "Seed extraction & memory injection & GraphRAG construction",
"step02Title": "Env Setup",
"step02Desc": "Entity extraction & persona generation & Agent config injection",
"step03Title": "Simulation",
"step03Desc": "Dual-platform parallel sim & auto-parse requirements & temporal memory",
"heroDescOptimalSolution": "sentiment trajectory",
"waveformLabelNoise": "raw signal",
"waveformLabelSignal": "extracted intelligence",
"heroPrimaryCta": "Initiate Assessment",
"heroSecondaryCta": "View Methodology",
"methodologyLabel": "Methodology",
"methodologyTitle": "Five-Phase Intelligence Cycle",
"methodologyDesc": "Every assessment passes through the same disciplined sequence, from document ingestion to deep interrogation of the simulated population.",
"step01Title": "Ingest",
"step01Desc": "Seed extraction, memory injection, and GraphRAG construction from source documents",
"step02Title": "Model",
"step02Desc": "Entity extraction, persona generation, and agent configuration injection",
"step03Title": "Simulate",
"step03Desc": "Dual-platform parallel simulation with temporal memory and auto-parsed requirements",
"step04Title": "Report",
"step04Desc": "ReportAgent interacts with the post-simulation environment via rich tools",
"step05Title": "Interaction",
"step05Desc": "Chat with any simulated individual & converse with ReportAgent",
"realitySeed": "01 / Reality Seed",
"supportedFormats": "Formats: PDF, MD, TXT",
"dragToUpload": "Drag files to upload",
"orBrowse": "or click to browse files",
"inputParams": "Input Parameters",
"simulationPrompt": ">_ 02 / Simulation Prompt",
"promptPlaceholder": "// Describe your simulation or prediction requirement in natural language",
"engineBadge": "Engine: MiroFish-V1.0",
"startEngine": "Start Engine",
"initializing": "Initializing..."
"step04Desc": "ReportAgent interacts with the post-simulation environment via a rich tool suite",
"step05Title": "Interrogate",
"step05Desc": "Chat directly with any simulated individual or converse with ReportAgent",
"deployLabel": "Deploy",
"deployTitle": "New Assessment",
"deployDesc": "Upload source documents and describe what you need to predict. The engine handles the rest.",
"realitySeed": "Source Documents",
"supportedFormats": "PDF · MD · TXT",
"dragToUpload": "Drag files here or click to browse",
"orBrowse": "No files selected — PDF, MD, or TXT accepted",
"simulationPrompt": "Assessment Brief",
"promptPlaceholder": "Describe the scenario you need to predict. e.g. 'If the central bank announces a 50bp rate cut, how will retail banking customers respond on social media over the following 72 hours?'",
"engineBadge": "MiroFish v1.0",
"startEngine": "Initiate Sentiment Assessment",
"initializing": "Initializing...",
"fileCount": "{count} files attached",
"removeFile": "Remove"
},
"main": {
"layoutGraph": "Graph",
@ -85,7 +87,7 @@
"ontologyDesc": "LLM analyzes document content and simulation requirements, extracts reality seeds, and auto-generates a suitable ontology structure",
"analyzingDocs": "Analyzing documents...",
"graphRagBuild": "GraphRAG Build",
"graphRagDesc": "Based on the generated ontology, documents are auto-chunked and sent to Zep to build a knowledge graph, extracting entities and relations, forming temporal memory and community summaries",
"graphRagDesc": "Based on the generated ontology, documents are auto-chunked and sent to the local graph store to build a knowledge graph, extracting entities and relations, forming temporal memory and community summaries",
"entityNodes": "Entity Nodes",
"relationEdges": "Relation Edges",
"schemaTypes": "Schema Types",
@ -297,6 +299,30 @@
"toggleMaximize": "Maximize/Restore",
"closeHint": "Close hint"
},
"process": {
"liveKnowledgeGraph": "Live Knowledge Graph",
"nodes": "nodes",
"relations": "relations",
"refreshGraph": "Refresh graph",
"exitFullscreen": "Exit fullscreen",
"enterFullscreen": "Fullscreen",
"buildingGraph": "Building graph...",
"buildingPhase": "Graph Build",
"graphBuildStarted": "Graph build task started...",
"startingGraphBuild": "Starting graph build...",
"graphBuildFailed": "Graph build failed",
"graphBuildFailedWithError": "Graph build failed: {error}",
"graphBuildComplete": "Graph build complete, loading full data...",
"unknownError": "unknown error",
"uploadDesc": "After uploading documents, the LLM analyzes content and auto-generates an ontology structure (entity types + relation types) suited for public opinion simulation",
"generatedRelationTypes": "Generated Relation Types ({count})",
"moreRelations": "+{count} more relations...",
"graphBuildPhaseDesc": "Based on the generated ontology, documents are chunked and sent to the graph engine to build a knowledge graph, extracting entities and relations",
"envSetupInProgress": "Environment setup feature in development...",
"unnamed": "Unnamed",
"unknown": "Unknown",
"startFailed": "Start failed"
},
"history": {
"title": "Simulation History",
"graphBuild": "Graph Build",
@ -328,7 +354,7 @@
"noDocProcessed": "No documents were processed successfully. Please check file formats.",
"requireProjectId": "Please provide project_id",
"configError": "Configuration error: {details}",
"zepApiKeyMissing": "ZEP_API_KEY not configured",
"graphApiKeyMissing": "Graph API key not configured",
"ontologyNotGenerated": "Ontology not yet generated. Please call /ontology/generate first.",
"graphBuilding": "Graph build in progress. Do not resubmit. To force rebuild, add force: true.",
"textNotFound": "Extracted text content not found",
@ -388,15 +414,45 @@
"envRunning": "Environment is running and ready for Interview commands",
"envNotRunningShort": "Environment not running or closed",
"requireGraphIdAndQuery": "Please provide graph_id and query",
"initReportAgent": "Initializing Report Agent..."
"initReportAgent": "Initializing Report Agent...",
"llmApiKeyMissing": "LLM_API_KEY not configured",
"ipcTimeout": "Timed out waiting for command response ({timeout}s)",
"simAlreadyRunning": "Simulation is already running: {id}",
"simConfigNotFoundPrepare": "Simulation config not found. Please call /prepare first.",
"graphMemoryRequiresGraphId": "Graph memory update requires a graph_id",
"scriptNotFound": "Script not found: {path}",
"simConfigNotFound": "Simulation config not found: {id}",
"simConfigNoAgents": "No agents in simulation config: {id}",
"fileNotFound": "File not found: {path}",
"unsupportedFileFormat": "Unsupported file format: {suffix}",
"unsupportedFileFormatGeneric": "Cannot process file format: {suffix}",
"llmJsonInvalid": "LLM returned invalid JSON format: {response}",
"simRunningHint": "Simulation is running. Stop it first, or use force=true to restart.",
"simNotRunning": "Simulation is not running: {id}, status={status}",
"envNotRunningInterview": "Simulation environment not running or closed. Cannot execute interview: {id}",
"simDirNotExist": "Simulation directory does not exist",
"simMissingFiles": "Missing required files",
"simNotPrepared": "Status not in prepared list or config_generated is false: status={status}, config_generated={config_generated}",
"simStateReadFailed": "Failed to read state file: {error}",
"envReadyForInterview": "Environment is running and ready for interview commands",
"envCloseCommandSent": "Environment close command sent",
"envCloseCommandSentTimeout": "Environment close command sent (response timed out, environment may be closing)",
"simDirNotExistNoClean": "Simulation directory does not exist, no cleanup needed",
"envAlreadyClosed": "Environment is already closed",
"unknownType": "Unknown type",
"unknown": "Unknown",
"unknownError": "Unknown error",
"unknownTool": "Unknown tool: {name}. Please use one of: insight_forge, panorama_search, quick_search",
"defaultReportTitle": "Simulation Analysis Report",
"unknownProfession": "Unknown profession"
},
"progress": {
"initGraphService": "Initializing graph build service...",
"textChunking": "Chunking text...",
"creatingZepGraph": "Creating Zep graph...",
"creatingGraph": "Creating knowledge graph...",
"settingOntology": "Setting ontology definition...",
"addingChunks": "Adding {count} text chunks...",
"waitingZepProcess": "Waiting for Zep to process data...",
"waitingGraphProcess": "Waiting for graph processing...",
"fetchingGraphData": "Fetching graph data...",
"graphBuildComplete": "Graph build complete",
"buildFailed": "Build failed: {error}",
@ -410,12 +466,13 @@
"noEpisodesWait": "No episodes to wait for",
"waitingEpisodes": "Waiting for {count} text chunks to process...",
"episodesTimeout": "Some chunks timed out, {completed}/{total} completed",
"zepProcessing": "Zep processing... {completed}/{total} done, {pending} pending ({elapsed}s)",
"graphProcessing": "Processing... {completed}/{total} done, {pending} pending ({elapsed}s)",
"processingComplete": "Processing complete: {completed}/{total}",
"extractingEntities": "Extracting entities via LLM... {done}/{total} chunks",
"taskComplete": "Task complete",
"taskFailed": "Task failed",
"startPreparingEnv": "Preparing simulation environment...",
"connectingZepGraph": "Connecting to Zep graph...",
"connectingGraph": "Connecting to graph store...",
"readingNodeData": "Reading node data...",
"readingComplete": "Done, {count} entities found",
"startGenerating": "Starting generation...",
@ -441,7 +498,7 @@
"generatingEventConfig": "Generating event config and hot topics...",
"generatingAgentConfig": "Generating agent config ({start}-{end}/{total})...",
"generatingPlatformConfig": "Generating platform config...",
"zepSearchQuery": "All information, activities, events, relationships and background about {name}",
"graphSearchQuery": "All information, activities, events, relationships and background about {name}",
"timeConfigLabel": "Time Config",
"eventConfigLabel": "Event Config",
"agentConfigResult": "Agent Config: {count} generated",
@ -492,7 +549,7 @@
"detectedExistingPrep": "Detected existing preparation, using it directly",
"prepareTaskStarted": "Preparation task started",
"prepareTaskId": " └─ Task ID: {taskId}",
"zepEntitiesFound": "Found {count} entities from Zep graph",
"graphEntitiesFound": "Found {count} entities from graph store",
"entityTypes": " └─ Entity types: {types}",
"startPollingProgress": "Polling preparation progress...",
"prepareFailed": "Preparation failed: {error}",
@ -612,13 +669,13 @@
"redirectToInsightForge": "get_simulation_context redirected to insight_forge"
},
"console": {
"zepToolsInitialized": "ZepToolsService initialized",
"zepRetryAttempt": "Zep {operation} attempt {attempt} failed: {error}, retrying in {delay}s...",
"zepAllRetriesFailed": "Zep {operation} failed after {retries} attempts: {error}",
"graphToolsInitialized": "GraphToolsService initialized",
"graphRetryAttempt": "{operation} attempt {attempt} failed: {error}, retrying in {delay}s...",
"graphAllRetriesFailed": "{operation} failed after {retries} attempts: {error}",
"graphSearch": "Graph search: graph_id={graphId}, query={query}...",
"graphSearchOp": "Graph search (graph={graphId})",
"searchComplete": "Search complete: found {count} relevant facts",
"zepSearchApiFallback": "Zep Search API failed, falling back to local search: {error}",
"graphSearchApiFallback": "Graph search API failed, falling back to local search: {error}",
"usingLocalSearch": "Using local search: query={query}...",
"localSearchComplete": "Local search complete: found {count} relevant facts",
"localSearchFailed": "Local search failed: {error}",

View File

@ -1,8 +1,4 @@
{
"zh": {
"label": "中文",
"llmInstruction": "请使用中文回答。"
},
"en": {
"label": "English",
"llmInstruction": "Please respond in English."

View File

@ -1,667 +0,0 @@
{
"common": {
"confirm": "确认",
"cancel": "取消",
"loading": "加载中...",
"error": "错误",
"success": "成功",
"completed": "已完成",
"processing": "生成中",
"pending": "等待",
"ready": "就绪",
"running": "运行中",
"failed": "失败",
"unknown": "未知",
"unknownError": "未知错误",
"none": "无",
"close": "关闭",
"back": "返回",
"next": "下一步",
"retry": "重试",
"noData": "暂无数据",
"hours": "小时",
"minutes": "分钟",
"rounds": "轮",
"items": "个",
"files": "个文件"
},
"meta": {
"title": "MiroFish - 预测万物",
"description": "MiroFish - 社交媒体舆论模拟系统"
},
"nav": {
"visitGithub": "访问我们的Github主页"
},
"home": {
"tagline": "简洁通用的群体智能引擎",
"version": "/ v0.1-预览版",
"heroTitle1": "上传任意报告",
"heroTitle2": "即刻推演未来",
"heroDesc": "即使只有一段文字,{brand} 也能基于其中的现实种子,全自动生成与之对应的至多{agentScale}构成的平行世界。通过上帝视角注入变量,在复杂的群体交互中寻找动态环境下的{optimalSolution}",
"heroDescBrand": "MiroFish",
"heroDescAgentScale": "百万级Agent",
"heroDescOptimalSolution": "\"局部最优解\"",
"slogan": "让未来在 Agent 群中预演,让决策在百战后胜出",
"systemStatus": "系统状态",
"systemReady": "准备就绪",
"systemReadyDesc": "预测引擎待命中,可上传多份非结构化数据以初始化模拟序列",
"metricLowCost": "低成本",
"metricLowCostDesc": "常规模拟平均5$/次",
"metricHighAvail": "高可用",
"metricHighAvailDesc": "最多百万级Agent模拟",
"workflowSequence": "工作流序列",
"step01Title": "图谱构建",
"step01Desc": "现实种子提取 & 个体与群体记忆注入 & GraphRAG构建",
"step02Title": "环境搭建",
"step02Desc": "实体关系抽取 & 人设生成 & 环境配置Agent注入仿真参数",
"step03Title": "开始模拟",
"step03Desc": "双平台并行模拟 & 自动解析预测需求 & 动态更新时序记忆",
"step04Title": "报告生成",
"step04Desc": "ReportAgent拥有丰富的工具集与模拟后环境进行深度交互",
"step05Title": "深度互动",
"step05Desc": "与模拟世界中的任意一位进行对话 & 与ReportAgent进行对话",
"realitySeed": "01 / 现实种子",
"supportedFormats": "支持格式: PDF, MD, TXT",
"dragToUpload": "拖拽文件上传",
"orBrowse": "或点击浏览文件系统",
"inputParams": "输入参数",
"simulationPrompt": ">_ 02 / 模拟提示词",
"promptPlaceholder": "// 用自然语言输入模拟或预测需求(例.武大若发布撤销肖某处分的公告,会引发什么舆情走向)",
"engineBadge": "引擎: MiroFish-V1.0",
"startEngine": "启动引擎",
"initializing": "初始化中..."
},
"main": {
"layoutGraph": "图谱",
"layoutSplit": "双栏",
"layoutWorkbench": "工作台",
"stepNames": ["图谱构建", "环境搭建", "开始模拟", "报告生成", "深度互动"]
},
"step1": {
"ontologyGeneration": "本体生成",
"ontologyCompleted": "已完成",
"ontologyGenerating": "生成中",
"ontologyPending": "等待",
"ontologyDesc": "LLM分析文档内容与模拟需求提取出现实种子自动生成合适的本体结构",
"analyzingDocs": "正在分析文档...",
"graphRagBuild": "GraphRAG构建",
"graphRagDesc": "基于生成的本体,将文档自动分块后调用 Zep 构建知识图谱,提取实体和关系,并形成时序记忆与社区摘要",
"entityNodes": "实体节点",
"relationEdges": "关系边",
"schemaTypes": "SCHEMA类型",
"buildComplete": "构建完成",
"buildCompleteDesc": "图谱构建已完成,请进入下一步进行模拟环境搭建",
"inProgress": "进行中",
"creating": "创建中...",
"enterEnvSetup": "进入环境搭建",
"createSimulationFailed": "创建模拟失败: {error}",
"createSimulationException": "创建模拟异常: {error}"
},
"step2": {
"simInstanceInit": "模拟实例初始化",
"simInstanceDesc": "新建simulation实例拉取模拟世界参数模版",
"asyncTaskDone": "异步任务已完成",
"generateAgentPersona": "生成 Agent 人设",
"generateAgentPersonaDesc": "结合上下文,自动调用工具从知识图谱梳理实体与关系,初始化模拟个体,并基于现实种子赋予他们独特的行为与记忆",
"currentAgentCount": "当前Agent数",
"expectedAgentTotal": "预期Agent总数",
"relatedTopicsCount": "现实种子当前关联话题数",
"generatedAgentPersonas": "已生成的 Agent 人设",
"unknownProfession": "未知职业",
"noBio": "暂无简介",
"dualPlatformConfig": "生成双平台模拟配置",
"dualPlatformConfigDesc": "LLM 根据模拟需求与现实种子,智能设置世界时间流速、推荐算法、每个个体的活跃时间段、发言频率、事件触发等参数",
"simulationDuration": "模拟时长",
"roundDuration": "每轮时长",
"totalRounds": "总轮次",
"activePerHour": "每小时活跃",
"peakHours": "高峰时段",
"workHours": "工作时段",
"morningHours": "早间时段",
"offPeakHours": "低谷时段",
"agentConfig": "Agent 配置",
"activeTimePeriod": "活跃时段",
"postsPerHour": "发帖/时",
"commentsPerHour": "评论/时",
"responseDelay": "响应延迟",
"activityLevel": "活跃度",
"sentimentBias": "情感倾向",
"influenceWeight": "影响力",
"recommendAlgoConfig": "推荐算法配置",
"platform1Name": "平台 1广场 / 信息流",
"platform2Name": "平台 2话题 / 社区",
"recencyWeight": "时效权重",
"popularityWeight": "热度权重",
"relevanceWeight": "相关性权重",
"viralThreshold": "病毒阈值",
"echoChamberStrength": "回音室强度",
"llmConfigReasoning": "LLM 配置推理",
"initialActivation": "初始激活编排",
"initialActivationDesc": "基于叙事方向,自动生成初始激活事件与热点话题,引导模拟世界的初始状态",
"orchestrating": "编排中",
"narrativeDirection": "叙事引导方向",
"initialHotTopics": "初始热点话题",
"initialActivationSeq": "初始激活序列 ({count})",
"setupComplete": "准备完成",
"setupCompleteDesc": "模拟环境已准备完成,可以开始运行模拟",
"roundsConfig": "模拟轮数设定",
"roundsConfigDesc": "MiroFish 自动规划推演现实 {hours} 小时,每轮代表现实 {minutesPerRound} 分钟时间流逝",
"customToggle": "自定义",
"roundsUnit": "轮",
"estimatedDuration": "若Agent规模为100预计耗时约 {minutes} 分钟",
"estimatedDurationFull": "若Agent规模为100预计耗时 {minutes} 分钟",
"recommendedRounds": "{rounds} (推荐)",
"customTip": "若首次运行,强烈建议切换至'自定义模式'减少模拟轮数,以便快速预览效果并降低报错风险",
"backToGraphBuild": "返回图谱构建",
"startDualWorldSim": "开始双世界并行模拟",
"profileModalAge": "事件外显年龄",
"profileModalGender": "事件外显性别",
"profileModalCountry": "国家/地区",
"profileModalMbti": "事件外显MBTI",
"profileModalBio": "人设简介",
"profileModalTopics": "现实种子关联话题",
"profileModalPersona": "详细人设背景",
"personaDimExperience": "事件全景经历",
"personaDimExperienceDesc": "在此事件中的完整行为轨迹",
"personaDimBehavior": "行为模式侧写",
"personaDimBehaviorDesc": "经验总结与行事风格偏好",
"personaDimMemory": "独特记忆印记",
"personaDimMemoryDesc": "基于现实种子形成的记忆",
"personaDimSocial": "社会关系网络",
"personaDimSocialDesc": "个体链接与交互图谱",
"genderMale": "男",
"genderFemale": "女",
"genderOther": "其他",
"yearsOld": "岁",
"initializing": "初始化",
"generating": "生成中"
},
"step3": {
"startGenerateReport": "开始生成结果报告",
"generatingReport": "启动中...",
"waitingForActions": "Waiting for agent actions...",
"errorMissingSimId": "错误:缺少 simulationId",
"startingDualSim": "正在启动双平台并行模拟...",
"graphMemoryUpdateEnabled": "已开启动态图谱更新模式",
"setMaxRounds": "设置最大模拟轮数: {rounds}",
"oldSimCleared": "已清理旧的模拟日志,重新开始模拟",
"engineStarted": "模拟引擎启动成功",
"startFailed": "启动失败: {error}",
"startException": "启动异常: {error}",
"stoppingSim": "正在停止模拟...",
"simStopped": "模拟已停止",
"stopFailed": "停止失败: {error}",
"stopException": "停止异常: {error}",
"allPlatformsCompleted": "检测到所有平台模拟已结束",
"simCompleted": "模拟已完成",
"graphRealtimeRefresh": "开启图谱实时刷新 (30s)",
"graphRefreshStopped": "停止图谱实时刷新",
"preparingGoBack": "准备返回 Step 2正在关闭模拟...",
"closingSimEnv": "正在关闭模拟环境...",
"simEnvClosed": "模拟环境已关闭",
"closeFailed": "关闭模拟环境失败,尝试强制停止...",
"stoppingProcess": "正在停止模拟进程...",
"checkStatusFailed": "检查模拟状态失败: {error}",
"forceStopSuccess": "模拟已强制停止",
"forceStopFailed": "强制停止失败: {error}",
"startGenerateReportBtn": "开始生成结果报告",
"generatingReportBtn": "启动中..."
},
"step4": {
"generatingSection": "正在生成{title}...",
"goToInteraction": "进入深度互动",
"waitingForReportAgent": "Waiting for Report Agent...",
"collapse": "收起 ▲",
"expandAll": "展开全部 {count} 条 ▼",
"expandAllEntities": "展开全部 {count} 个 ▼",
"scenarioLabel": "预测场景: ",
"tabKeyFacts": "当前关键记忆 ({count})",
"tabCoreEntities": "核心实体 ({count})",
"tabRelationChains": "关系链 ({count})",
"tabSubQueries": "子问题 ({count})",
"panelKeyFacts": "时序记忆中所关联的最新关键事实",
"totalCount": "共 {count} 条",
"totalEntityCount": "共 {count} 个",
"panelCoreEntities": "核心实体",
"factCount": "{count}条",
"panelRelationChains": "关系链",
"panelSubQueries": "漂移查询生成分析子问题",
"emptyKeyFacts": "暂无当前关键记忆",
"emptyCoreEntities": "暂无核心实体",
"emptyRelationChains": "暂无关系链",
"tabActiveFacts": "当前有效记忆 ({count})",
"tabHistoricalFacts": "历史记忆 ({count})",
"tabEntities": "涉及实体 ({count})",
"panelActiveFacts": "当前有效记忆",
"emptyActiveFacts": "暂无当前有效记忆",
"panelHistoricalFacts": "历史记忆",
"emptyHistoricalFacts": "暂无历史记忆",
"panelEntities": "涉及实体",
"emptyEntities": "暂无涉及实体",
"searchLabel": "搜索: ",
"tabFacts": "事实 ({count})",
"tabEdges": "关系 ({count})",
"tabNodes": "节点 ({count})",
"panelSearchResults": "搜索结果",
"emptySearchResults": "未找到相关结果",
"panelRelatedEdges": "相关关系",
"panelRelatedNodes": "相关节点",
"world1": "世界1",
"world2": "世界2"
},
"step5": {
"interactiveTools": "Interactive Tools",
"agentsAvailable": "{count} agents available",
"chatWithReportAgent": "与Report Agent对话",
"chatWithAgent": "与世界中任意个体对话",
"selectChatTarget": "选择对话对象",
"sendSurvey": "发送问卷调查到世界中",
"reportAgentChat": "Report Agent - Chat",
"reportAgentDesc": "报告生成智能体的快速对话版本,可调用 4 种专业工具拥有MiroFish的完整记忆",
"toolInsightForge": "InsightForge 深度归因",
"toolInsightForgeDesc": "对齐现实世界种子数据与模拟环境状态结合Global/Local Memory机制提供跨时空的深度归因分析",
"toolPanoramaSearch": "PanoramaSearch 全景追踪",
"toolPanoramaSearchDesc": "基于图结构的广度遍历算法,重构事件传播路径,捕获全量信息流动的拓扑结构",
"toolQuickSearch": "QuickSearch 快速检索",
"toolQuickSearchDesc": "基于 GraphRAG 的即时查询接口,优化索引效率,用于快速提取具体的节点属性与离散事实",
"toolInterviewSubAgent": "InterviewSubAgent 虚拟访谈",
"toolInterviewSubAgentDesc": "自主式访谈,能够并行与模拟世界中个体进行多轮对话,采集非结构化的观点数据与心理状态",
"profileBio": "简介",
"chatEmptyReportAgent": "与 Report Agent 对话,深入了解报告内容",
"chatEmptyAgent": "与模拟个体对话,了解他们的观点",
"chatInputPlaceholder": "输入您的问题...",
"selectSurveyTarget": "选择调查对象",
"selectedCount": "已选 {selected} / {total}",
"surveyQuestions": "问卷问题",
"surveyInputPlaceholder": "输入您想问所有被选中对象的问题...",
"submitSurvey": "发送问卷",
"surveyResults": "调查结果",
"surveyResultsCount": "{count} 条回复",
"selectAll": "全选",
"clearSelection": "清空",
"errorOccurred": "抱歉,发生了错误: {error}",
"noResponse": "无响应",
"requestFailed": "请求失败",
"selectAgentFirst": "请先选择一个模拟个体"
},
"graph": {
"panelTitle": "Graph Relationship Visualization",
"refreshGraph": "刷新图谱",
"graphMemoryRealtime": "GraphRAG长短期记忆实时更新中",
"realtimeUpdating": "实时更新中...",
"pendingContentHint": "还有少量内容处理中,建议稍后手动刷新图谱",
"nodeDetails": "Node Details",
"relationship": "Relationship",
"graphDataLoading": "图谱数据加载中...",
"waitingOntology": "等待本体生成...",
"toggleMaximize": "最大化/还原",
"closeHint": "关闭提示"
},
"history": {
"title": "推演记录",
"graphBuild": "图谱构建",
"envSetup": "环境搭建",
"analysisReport": "分析报告",
"moreFiles": "+{count} 个文件",
"noFiles": "暂无文件",
"loadingText": "加载中...",
"simRequirement": "模拟需求",
"relatedFiles": "关联文件",
"noRelatedFiles": "暂无关联文件",
"replayTitle": "推演回放",
"step1Button": "图谱构建",
"step2Button": "环境搭建",
"step4Button": "分析报告",
"replayHint": "Step3「开始模拟」与 Step5「深度互动」需在运行中启动不支持历史回放",
"notStarted": "未开始",
"roundsProgress": "{current}/{total} 轮",
"untitledSimulation": "未命名模拟",
"unknownFile": "未知文件"
},
"api": {
"projectNotFound": "项目不存在: {id}",
"projectDeleteFailed": "项目不存在或删除失败: {id}",
"projectDeleted": "项目已删除: {id}",
"projectReset": "项目已重置: {id}",
"requireSimulationRequirement": "请提供模拟需求描述 (simulation_requirement)",
"requireFileUpload": "请至少上传一个文档文件",
"noDocProcessed": "没有成功处理任何文档,请检查文件格式",
"requireProjectId": "请提供 project_id",
"configError": "配置错误: {details}",
"zepApiKeyMissing": "ZEP_API_KEY未配置",
"ontologyNotGenerated": "项目尚未生成本体,请先调用 /ontology/generate",
"graphBuilding": "图谱正在构建中,请勿重复提交。如需强制重建,请添加 force: true",
"textNotFound": "未找到提取的文本内容",
"ontologyNotFound": "未找到本体定义",
"graphBuildStarted": "图谱构建任务已启动,请通过 /task/{taskId} 查询进度",
"graphBuildComplete": "图谱构建完成",
"buildFailed": "构建失败: {error}",
"taskNotFound": "任务不存在: {id}",
"graphDeleted": "图谱已删除: {id}",
"entityNotFound": "实体不存在: {id}",
"graphNotBuilt": "项目尚未构建图谱,请先调用 /api/graph/build",
"requireSimulationId": "请提供 simulation_id",
"simulationNotFound": "模拟不存在: {id}",
"projectMissingRequirement": "项目缺少模拟需求描述 (simulation_requirement)",
"prepareStarted": "准备任务已启动,请通过 /api/simulation/prepare/status 查询进度",
"alreadyPrepared": "已有完成的准备工作,无需重复生成",
"notStartedPrepare": "尚未开始准备,请调用 /api/simulation/prepare 开始",
"taskCompletedPrepared": "任务已完成(准备工作已存在)",
"requireTaskOrSimId": "请提供 task_id 或 simulation_id",
"configNotFound": "模拟配置不存在,请先调用 /prepare 接口",
"configFileNotFound": "配置文件不存在,请先调用 /prepare 接口",
"unknownScript": "未知脚本: {name},可选: {allowed}",
"scriptFileNotFound": "脚本文件不存在: {name}",
"requireGraphId": "请提供 graph_id",
"noMatchingEntities": "没有找到符合条件的实体",
"maxRoundsPositive": "max_rounds 必须是正整数",
"maxRoundsInvalid": "max_rounds 必须是有效的整数",
"invalidPlatform": "无效的平台类型: {platform},可选: twitter/reddit/parallel",
"simRunningForceHint": "模拟正在运行中,请先调用 /stop 接口停止,或使用 force=true 强制重新开始",
"simNotReady": "模拟未准备好,当前状态: {status},请先调用 /prepare 接口",
"graphIdRequiredForMemory": "启用图谱记忆更新需要有效的 graph_id请确保项目已构建图谱",
"dbNotExist": "数据库不存在,模拟可能尚未运行",
"requireMessage": "请提供 message",
"missingGraphId": "缺少图谱ID",
"missingGraphIdEnsure": "缺少图谱ID请确保已构建图谱",
"missingSimRequirement": "缺少模拟需求描述",
"reportAlreadyExists": "报告已存在",
"reportGenerateStarted": "报告生成任务已启动,请通过 /api/report/generate/status 查询进度",
"reportGenerated": "报告已生成",
"reportNotFound": "报告不存在: {id}",
"noReportForSim": "该模拟暂无报告: {id}",
"reportDeleted": "报告已删除: {id}",
"reportGenerateFailed": "报告生成失败",
"sectionNotFound": "章节不存在: section_{index}.md",
"reportProgressNotAvail": "报告不存在或进度信息不可用: {id}",
"requireAgentId": "请提供 agent_id",
"requirePrompt": "请提供 prompt采访问题",
"invalidInterviewPlatform": "platform 参数只能是 'twitter' 或 'reddit'",
"envNotRunning": "模拟环境未运行或已关闭。请确保模拟已完成并进入等待命令模式。",
"interviewTimeout": "等待Interview响应超时: {error}",
"requireInterviews": "请提供 interviews采访列表",
"interviewListMissingAgentId": "采访列表第{index}项缺少 agent_id",
"interviewListMissingPrompt": "采访列表第{index}项缺少 prompt",
"interviewListInvalidPlatform": "采访列表第{index}项的platform只能是 'twitter' 或 'reddit'",
"batchInterviewTimeout": "等待批量Interview响应超时: {error}",
"globalInterviewTimeout": "等待全局Interview响应超时: {error}",
"envRunning": "环境正在运行可以接收Interview命令",
"envNotRunningShort": "环境未运行或已关闭",
"requireGraphIdAndQuery": "请提供 graph_id 和 query",
"initReportAgent": "初始化Report Agent..."
},
"progress": {
"initGraphService": "初始化图谱构建服务...",
"textChunking": "文本分块中...",
"creatingZepGraph": "创建Zep图谱...",
"settingOntology": "设置本体定义...",
"addingChunks": "开始添加 {count} 个文本块...",
"waitingZepProcess": "等待Zep处理数据...",
"fetchingGraphData": "获取图谱数据...",
"graphBuildComplete": "图谱构建完成",
"buildFailed": "构建失败: {error}",
"startBuildingGraph": "开始构建图谱...",
"graphCreated": "图谱已创建: {graphId}",
"ontologySet": "本体已设置",
"textSplit": "文本已分割为 {count} 个块",
"fetchingGraphInfo": "获取图谱信息...",
"sendingBatch": "发送第 {current}/{total} 批数据 ({chunks} 块)...",
"batchFailed": "批次 {batch} 发送失败: {error}",
"noEpisodesWait": "无需等待(没有 episode",
"waitingEpisodes": "开始等待 {count} 个文本块处理...",
"episodesTimeout": "部分文本块超时,已完成 {completed}/{total}",
"zepProcessing": "Zep处理中... {completed}/{total} 完成, {pending} 待处理 ({elapsed}秒)",
"processingComplete": "处理完成: {completed}/{total}",
"taskComplete": "任务完成",
"taskFailed": "任务失败",
"startPreparingEnv": "开始准备模拟环境...",
"connectingZepGraph": "正在连接Zep图谱...",
"readingNodeData": "正在读取节点数据...",
"readingComplete": "完成,共 {count} 个实体",
"startGenerating": "开始生成...",
"analyzingRequirements": "正在分析模拟需求...",
"generatingOutline": "正在生成报告大纲...",
"parsingOutline": "正在解析大纲结构...",
"outlinePlanComplete": "大纲规划完成",
"deepSearchAndWrite": "深度检索与撰写中 ({current}/{max})",
"initReport": "初始化报告...",
"startPlanningOutline": "开始规划报告大纲...",
"outlineDone": "大纲规划完成,共{count}个章节",
"generatingSection": "正在生成章节: {title} ({current}/{total})",
"sectionDone": "章节 {title} 已完成",
"assemblingReport": "正在组装完整报告...",
"reportComplete": "报告生成完成",
"reportFailed": "报告生成失败: {error}",
"savingProfiles": "保存Profile文件...",
"profilesComplete": "完成,共 {count} 个Profile",
"callingLLMConfig": "正在调用LLM生成配置...",
"savingConfigFiles": "正在保存配置文件...",
"configComplete": "配置生成完成",
"generatingTimeConfig": "生成时间配置...",
"generatingEventConfig": "生成事件配置和热点话题...",
"generatingAgentConfig": "生成Agent配置 ({start}-{end}/{total})...",
"generatingPlatformConfig": "生成平台配置...",
"zepSearchQuery": "关于{name}的所有信息、活动、事件、关系和背景",
"timeConfigLabel": "时间配置",
"eventConfigLabel": "事件配置",
"agentConfigResult": "Agent配置: 成功生成 {count} 个",
"postAssignResult": "初始帖子分配: {count} 个帖子已分配发布者",
"profileGenerated": "[已生成] {name} ({type})",
"readingGraphEntities": "读取图谱实体",
"generatingProfiles": "生成Agent人设",
"generatingSimConfig": "生成模拟配置",
"preparingScripts": "准备模拟脚本"
},
"log": {
"preparingGoBack": "准备返回 Step 2正在关闭模拟...",
"closingSimEnv": "正在关闭模拟环境...",
"simEnvClosed": "✓ 模拟环境已关闭",
"closeSimEnvFailed": "关闭模拟环境失败,尝试强制停止...",
"simForceStopSuccess": "✓ 模拟已强制停止",
"forceStopFailed": "强制停止失败: {error}",
"stoppingSimProcess": "正在停止模拟进程...",
"simStopped": "✓ 模拟已停止",
"stopSimFailed": "停止模拟失败: {error}",
"checkStatusFailed": "检查模拟状态失败: {error}",
"enterStep4": "进入 Step 4: 报告生成",
"loadingSimData": "加载模拟数据: {id}",
"timeConfig": "时间配置: 每轮 {minutes} 分钟",
"timeConfigFetchFailed": "获取时间配置失败,使用默认值: {minutes}分钟/轮",
"projectLoadSuccess": "项目加载成功: {id}",
"loadSimDataFailed": "加载模拟数据失败: {error}",
"loadException": "加载异常: {error}",
"graphDataLoadSuccess": "图谱数据加载成功",
"graphLoadFailed": "图谱加载失败: {error}",
"graphRealtimeRefreshStart": "开启图谱实时刷新 (30s)",
"graphRealtimeRefreshStop": "停止图谱实时刷新",
"simRunViewInit": "SimulationRunView 初始化",
"customRounds": "自定义模拟轮数: {rounds}",
"enterStep3": "进入 Step 3: 开始模拟",
"customRoundsConfig": "自定义模拟轮数: {rounds} 轮",
"useAutoRounds": "使用自动配置的模拟轮数",
"detectedSimEnvRunning": "检测到模拟环境正在运行,正在关闭...",
"closeSimEnvFailedWithError": "关闭模拟环境失败: {error}",
"closeSimEnvException": "关闭模拟环境异常: {error}",
"detectedSimRunning": "检测到模拟状态为运行中,正在停止...",
"forceStopSimFailed": "强制停止模拟失败: {error}",
"forceStopSimException": "强制停止模拟异常: {error}",
"simViewInit": "SimulationView 初始化",
"errorMissingSimId": "错误:缺少 simulationId",
"simInstanceCreated": "模拟实例已创建: {id}",
"preparingSimEnv": "正在准备模拟环境...",
"detectedExistingPrep": "检测到已有完成的准备工作,直接使用",
"prepareTaskStarted": "准备任务已启动",
"prepareTaskId": " └─ Task ID: {taskId}",
"zepEntitiesFound": "从Zep图谱读取到 {count} 个实体",
"entityTypes": " └─ 实体类型: {types}",
"startPollingProgress": "开始轮询准备进度...",
"prepareFailed": "准备失败: {error}",
"prepareException": "准备异常: {error}",
"prepareComplete": "✓ 准备工作已完成",
"prepareFailedWithError": "✗ 准备失败: {error}",
"startGeneratingConfig": "开始生成双平台模拟配置...",
"generatingAgentProfileConfig": "正在生成Agent人设配置...",
"generatingLLMConfig": "正在调用LLM生成模拟配置参数...",
"configComplete": "✓ 模拟配置生成完成",
"configSummaryAgents": " ├─ Agent数量: {count}个",
"configSummaryHours": " ├─ 模拟时长: {hours}小时",
"configSummaryPosts": " ├─ 初始帖子: {count}条",
"configSummaryTopics": " ├─ 热点话题: {count}个",
"configSummaryPlatforms": " └─ 平台配置: Twitter {twitter}, Reddit {reddit}",
"timeConfigDetail": "时间配置: 每轮{minutes}分钟, 共{rounds}轮",
"narrativeDirection": "叙事方向: {direction}",
"envSetupComplete": "✓ 环境搭建完成,可以开始模拟",
"startSimCustomRounds": "开始模拟,自定义轮数: {rounds} 轮",
"startSimAutoRounds": "开始模拟,使用自动配置轮数: {rounds} 轮",
"startGeneratingAgentProfiles": "开始生成Agent人设...",
"agentProfile": "→ Agent人设 {current}/{total}: {name} ({profession})",
"allProfilesComplete": "✓ 全部 {count} 个Agent人设生成完成",
"loadingExistingConfig": "正在加载已有配置数据...",
"loadedAgentProfiles": "已加载 {count} 个Agent人设",
"configLoadSuccess": "✓ 模拟配置加载成功",
"configSummaryPostsAlt": " └─ 初始帖子: {count}条",
"configGenerating": "配置生成中,开始轮询等待...",
"configNotGenerating": "模拟配置尚未生成,且后端已不再生成配置",
"loadConfigFailed": "加载配置失败: {error}",
"step2Init": "Step2 环境搭建初始化",
"step3Init": "Step3 模拟运行初始化",
"startingDualSim": "正在启动双平台并行模拟...",
"setMaxRounds": "设置最大模拟轮数: {rounds}",
"graphMemoryUpdateEnabled": "已开启动态图谱更新模式",
"oldSimCleared": "✓ 已清理旧的模拟日志,重新开始模拟",
"engineStarted": "✓ 模拟引擎启动成功",
"startFailed": "✗ 启动失败: {error}",
"startException": "✗ 启动异常: {error}",
"stoppingSim": "正在停止模拟...",
"simStoppedSuccess": "✓ 模拟已停止",
"stopFailed": "停止失败: {error}",
"stopException": "停止异常: {error}",
"allPlatformsCompleted": "✓ 检测到所有平台模拟已结束",
"simCompleted": "✓ 模拟已完成",
"simFailed": "✗ 模拟失败",
"reportRequestSent": "报告生成请求已发送,请稍候...",
"startingReportGen": "正在启动报告生成...",
"reportGenTaskStarted": "✓ 报告生成任务已启动: {reportId}",
"reportGenFailed": "✗ 启动报告生成失败: {error}",
"reportGenException": "✗ 启动报告生成异常: {error}",
"step5Init": "Step5 深度互动初始化",
"selectChatTarget": "选择对话对象: {name}",
"sendFailed": "发送失败: {error}",
"sendToReportAgent": "向 Report Agent 发送: {message}...",
"reportAgentReplied": "Report Agent 已回复",
"sendToAgent": "向 {name} 发送: {message}...",
"agentReplied": "{name} 已回复",
"sendSurvey": "发送问卷给 {count} 个对象...",
"receivedReplies": "收到 {count} 条回复",
"surveySendFailed": "问卷发送失败: {error}",
"loadReportData": "加载报告数据: {id}",
"loadReportFailed": "加载报告失败: {error}",
"reportDataLoaded": "报告数据加载完成",
"loadReportLogFailed": "加载报告日志失败: {error}",
"loadedProfiles": "加载了 {count} 个模拟个体",
"loadProfilesFailed": "加载模拟个体失败: {error}",
"interactionViewInit": "InteractionView 初始化",
"reportViewInit": "ReportView 初始化",
"getReportInfoFailed": "获取报告信息失败: {error}",
"enterStep": "进入 Step {step}: {name}",
"returnToStep": "返回 Step {step}: {name}",
"customSimRounds": "自定义模拟轮数: {rounds} 轮"
},
"report": {
"taskStarted": "报告生成任务开始",
"planningStart": "开始规划报告大纲",
"fetchSimContext": "获取模拟上下文信息",
"planningComplete": "大纲规划完成",
"sectionStart": "开始生成章节: {title}",
"reactThought": "ReACT 第{iteration}轮思考",
"toolCall": "调用工具: {toolName}",
"toolResult": "工具 {toolName} 返回结果",
"llmResponse": "LLM 响应 (工具调用: {hasToolCalls}, 最终答案: {hasFinalAnswer})",
"sectionContentDone": "章节 {title} 内容生成完成",
"sectionComplete": "章节 {title} 生成完成",
"reportComplete": "报告生成完成",
"errorOccurred": "发生错误: {error}",
"agentInitDone": "ReportAgent 初始化完成: graph_id={graphId}, simulation_id={simulationId}",
"executingTool": "执行工具: {toolName}, 参数: {params}",
"toolExecFailed": "工具执行失败: {toolName}, 错误: {error}",
"startPlanningOutline": "开始规划报告大纲...",
"outlinePlanDone": "大纲规划完成: {count} 个章节",
"outlinePlanFailed": "大纲规划失败: {error}",
"reactGenerateSection": "ReACT生成章节: {title}",
"sectionIterNone": "章节 {title} 第 {iteration} 次迭代: LLM 返回 None",
"sectionConflict": "章节 {title} 第 {iteration} 轮: LLM 同时输出工具调用和 Final Answer第 {conflictCount} 次冲突)",
"sectionConflictDowngrade": "章节 {title}: 连续 {conflictCount} 次冲突,降级为截断执行第一个工具调用",
"sectionGenDone": "章节 {title} 生成完成(工具调用: {count}次)",
"multiToolOnlyFirst": "LLM 尝试调用 {total} 个工具,只执行第一个: {toolName}",
"sectionNoPrefix": "章节 {title} 未检测到 'Final Answer:' 前缀直接采纳LLM输出作为最终内容工具调用: {count}次)",
"sectionMaxIter": "章节 {title} 达到最大迭代次数,强制生成",
"sectionForceFailed": "章节 {title} 强制收尾时 LLM 返回 None使用默认错误提示",
"sectionGenFailedContent": "本章节生成失败LLM 返回空响应,请稍后重试)",
"outlineSavedToFile": "大纲已保存到文件: {reportId}/outline.json",
"sectionSaved": "章节已保存: {reportId}/section_{sectionNum}.md",
"reportGenDone": "报告生成完成: {reportId}",
"reportGenFailed": "报告生成失败: {error}",
"agentChat": "Report Agent对话: {message}...",
"fetchReportFailed": "获取报告内容失败: {error}",
"outlineSaved": "大纲已保存: {reportId}",
"sectionFileSaved": "章节已保存: {reportId}/{fileSuffix}",
"fullReportAssembled": "完整报告已组装: {reportId}",
"reportSaved": "报告已保存: {reportId}",
"reportFolderDeleted": "报告文件夹已删除: {reportId}",
"redirectToQuickSearch": "search_graph 已重定向到 quick_search",
"redirectToInsightForge": "get_simulation_context 已重定向到 insight_forge"
},
"console": {
"zepToolsInitialized": "ZepToolsService 初始化完成",
"zepRetryAttempt": "Zep {operation} 第 {attempt} 次尝试失败: {error}, {delay}秒后重试...",
"zepAllRetriesFailed": "Zep {operation} 在 {retries} 次尝试后仍失败: {error}",
"graphSearch": "图谱搜索: graph_id={graphId}, query={query}...",
"graphSearchOp": "图谱搜索(graph={graphId})",
"searchComplete": "搜索完成: 找到 {count} 条相关事实",
"zepSearchApiFallback": "Zep Search API失败降级为本地搜索: {error}",
"usingLocalSearch": "使用本地搜索: query={query}...",
"localSearchComplete": "本地搜索完成: 找到 {count} 条相关事实",
"localSearchFailed": "本地搜索失败: {error}",
"fetchingAllNodes": "获取图谱 {graphId} 的所有节点...",
"fetchedNodes": "获取到 {count} 个节点",
"fetchingAllEdges": "获取图谱 {graphId} 的所有边...",
"fetchedEdges": "获取到 {count} 条边",
"fetchingNodeDetail": "获取节点详情: {uuid}...",
"fetchNodeDetailOp": "获取节点详情(uuid={uuid}...)",
"fetchNodeDetailFailed": "获取节点详情失败: {error}",
"fetchingNodeEdges": "获取节点 {uuid}... 的相关边",
"foundNodeEdges": "找到 {count} 条与节点相关的边",
"fetchNodeEdgesFailed": "获取节点边失败: {error}",
"fetchingEntitiesByType": "获取类型为 {type} 的实体...",
"foundEntitiesByType": "找到 {count} 个 {type} 类型的实体",
"fetchingEntitySummary": "获取实体 {name} 的关系摘要...",
"fetchingGraphStats": "获取图谱 {graphId} 的统计信息...",
"fetchingSimContext": "获取模拟上下文: {requirement}...",
"insightForgeStart": "InsightForge 深度洞察检索: {query}...",
"generatedSubQueries": "生成 {count} 个子问题",
"insightForgeComplete": "InsightForge完成: {facts}条事实, {entities}个实体, {relationships}条关系",
"generateSubQueriesFailed": "生成子问题失败: {error},使用默认子问题",
"panoramaSearchStart": "PanoramaSearch 广度搜索: {query}...",
"panoramaSearchComplete": "PanoramaSearch完成: {active}条有效, {historical}条历史",
"quickSearchStart": "QuickSearch 简单搜索: {query}...",
"quickSearchComplete": "QuickSearch完成: {count}条结果",
"interviewAgentsStart": "InterviewAgents 深度采访真实API: {requirement}...",
"profilesNotFound": "未找到模拟 {simId} 的人设文件",
"loadedProfiles": "加载到 {count} 个Agent人设",
"selectedAgentsForInterview": "选择了 {count} 个Agent进行采访: {indices}",
"generatedInterviewQuestions": "生成了 {count} 个采访问题",
"callingBatchInterviewApi": "调用批量采访API双平台: {count} 个Agent",
"interviewApiReturned": "采访API返回: {count} 个结果, success={success}",
"interviewApiReturnedFailure": "采访API返回失败: {error}",
"interviewApiCallFailed": "采访API调用失败环境未运行: {error}",
"interviewApiCallException": "采访API调用异常: {error}",
"interviewAgentsComplete": "InterviewAgents完成: 采访了 {count} 个Agent双平台",
"loadedRedditProfiles": "从 reddit_profiles.json 加载了 {count} 个人设",
"readRedditProfilesFailed": "读取 reddit_profiles.json 失败: {error}",
"loadedTwitterProfiles": "从 twitter_profiles.csv 加载了 {count} 个人设",
"readTwitterProfilesFailed": "读取 twitter_profiles.csv 失败: {error}",
"llmSelectAgentFailed": "LLM选择Agent失败使用默认选择: {error}",
"generateInterviewQuestionsFailed": "生成采访问题失败: {error}",
"generateInterviewSummaryFailed": "生成采访摘要失败: {error}"
}
}

View File

@ -1,7 +1,7 @@
{
"name": "mirofish",
"version": "0.1.0",
"description": "MiroFish - 简洁通用的群体智能引擎,预测万物",
"description": "MiroFish - A Simple and Universal Swarm Intelligence Engine, Predicting Anything",
"scripts": {
"setup": "npm install && cd frontend && npm install",
"setup:backend": "cd backend && uv sync",

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 246 KiB

After

Width:  |  Height:  |  Size: 246 KiB

View File

Before

Width:  |  Height:  |  Size: 255 KiB

After

Width:  |  Height:  |  Size: 255 KiB

View File

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

View File

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

View File

Before

Width:  |  Height:  |  Size: 450 KiB

After

Width:  |  Height:  |  Size: 450 KiB

View File

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 454 KiB

After

Width:  |  Height:  |  Size: 454 KiB

View File

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 199 KiB