diff --git a/Dockerfile b/Dockerfile index e6564686..fa3be94a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,78 @@ -FROM python:3.11 +# MiroFish production Dockerfile for agent.profikid.nl deployment +# - Builds the Vue frontend into static files +# - Serves the static bundle via nginx on :3000 +# - Reverse-proxies /api/* to the Flask backend on :5001 (same container, localhost) +# - Single exposed port for Traefik +# - Memory graph: Graphiti (Python lib) talks to FalkorDB on the shared network -# 安装 Node.js (满足 >=18)及必要工具 +FROM python:3.11-slim + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + VITE_API_BASE_URL=/api \ + HF_HUB_OFFLINE=0 \ + SENTENCE_TRANSFORMERS_HOME=/root/.cache/huggingface + +# Install Node 20 (for building the frontend), nginx (for serving it), and +# build deps for the Python wheels (sentence-transformers pulls numpy/torch). RUN apt-get update \ - && apt-get install -y --no-install-recommends nodejs npm \ - && rm -rf /var/lib/apt/lists/* + && apt-get install -y --no-install-recommends \ + nodejs \ + npm \ + nginx \ + curl \ + ca-certificates \ + git \ + && rm -rf /var/lib/apt/lists/* -# 从 uv 官方镜像复制 uv +# Install uv for fast Python package management COPY --from=ghcr.io/astral-sh/uv:0.9.26 /uv /uvx /bin/ WORKDIR /app -# 先复制依赖描述文件以利用缓存 -COPY package.json package-lock.json ./ +# ---- 1) Install Python deps (backend) ---- +# Copy the local path-dep (camel-oasis fork) BEFORE uv sync, so the path can resolve. +# Note: we don't COPY uv.lock here — the first build has to generate it. Once a +# successful build has happened, the lock will be in the source tree and a +# subsequent `uv sync --frozen` will be reproducible. +COPY backend/vendor ./backend/vendor +COPY backend/pyproject.toml ./backend/ +WORKDIR /app/backend +RUN uv sync + +# ---- 2) Install Node deps + build frontend ---- +WORKDIR /app + +# Copy locales first because the Vite build's `@locales` alias points to ../locales +COPY locales/ ./locales/ + COPY frontend/package.json frontend/package-lock.json ./frontend/ -COPY backend/pyproject.toml backend/uv.lock ./backend/ +RUN cd frontend && npm ci --no-audit --no-fund -# 安装依赖(Node + Python) -RUN npm ci \ - && npm ci --prefix frontend \ - && cd backend && uv sync --frozen +# Copy full frontend source and build the production bundle +COPY frontend/ ./frontend/ +RUN cd frontend && npm run build -# 复制项目源码 -COPY . . +# ---- 3) Copy the rest of the app (backend source) ---- +COPY backend/ ./backend/ -EXPOSE 3000 5001 +# ---- 4) Pre-download the sentence-transformers embedding model ---- +# Doing it at build time means the container is ready to serve on first boot +# (no 100MB download the first time the user runs a graph build). +WORKDIR /app/backend +RUN uv run python -c "from sentence_transformers import SentenceTransformer; \ + SentenceTransformer('sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2')" \ + || (echo 'Failed to pre-download embedding model — build will fall back to download on first use.' && exit 0) -# 同时启动前后端(开发模式) -CMD ["npm", "run", "dev"] \ No newline at end of file +# ---- 5) nginx config + start script ---- +COPY nginx.conf /etc/nginx/nginx.conf +COPY start.sh /app/start.sh +RUN chmod +x /app/start.sh + +# Runtime dirs +RUN mkdir -p /app/backend/uploads /app/logs /var/lib/nginx /var/log/nginx + +EXPOSE 3000 + +CMD ["/app/start.sh"] diff --git a/backend/app/api/graph.py b/backend/app/api/graph.py index 759ff48b..f1153f18 100644 --- a/backend/app/api/graph.py +++ b/backend/app/api/graph.py @@ -285,7 +285,7 @@ def build_graph(): # 检查配置 errors = [] - if not Config.ZEP_API_KEY: + if not Config.FALKORDB_HOST: errors.append(t('api.zepApiKeyMissing')) if errors: logger.error(f"配置错误: {errors}") @@ -387,7 +387,7 @@ def build_graph(): ) # 创建图谱构建服务 - builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) + builder = GraphBuilderService(api_key=None) # 分块 task_manager.update_task( @@ -572,13 +572,13 @@ def get_graph_data(graph_id: str): 获取图谱数据(节点和边) """ try: - if not Config.ZEP_API_KEY: + if not Config.FALKORDB_HOST: return jsonify({ "success": False, "error": t('api.zepApiKeyMissing') }), 500 - builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) + builder = GraphBuilderService(api_key=None) graph_data = builder.get_graph_data(graph_id) return jsonify({ @@ -600,13 +600,13 @@ def delete_graph(graph_id: str): 删除Zep图谱 """ try: - if not Config.ZEP_API_KEY: + if not Config.FALKORDB_HOST: return jsonify({ "success": False, "error": t('api.zepApiKeyMissing') }), 500 - builder = GraphBuilderService(api_key=Config.ZEP_API_KEY) + builder = GraphBuilderService(api_key=None) builder.delete_graph(graph_id) return jsonify({ diff --git a/backend/app/api/simulation.py b/backend/app/api/simulation.py index 3a8e1e3f..d52d7030 100644 --- a/backend/app/api/simulation.py +++ b/backend/app/api/simulation.py @@ -57,7 +57,7 @@ def get_graph_entities(graph_id: str): enrich: 是否获取相关边信息(默认true) """ try: - if not Config.ZEP_API_KEY: + if not Config.FALKORDB_HOST: return jsonify({ "success": False, "error": t('api.zepApiKeyMissing') @@ -94,7 +94,7 @@ def get_graph_entities(graph_id: str): def get_entity_detail(graph_id: str, entity_uuid: str): """获取单个实体的详细信息""" try: - if not Config.ZEP_API_KEY: + if not Config.FALKORDB_HOST: return jsonify({ "success": False, "error": t('api.zepApiKeyMissing') @@ -127,7 +127,7 @@ def get_entity_detail(graph_id: str, entity_uuid: str): def get_entities_by_type(graph_id: str, entity_type: str): """获取指定类型的所有实体""" try: - if not Config.ZEP_API_KEY: + if not Config.FALKORDB_HOST: return jsonify({ "success": False, "error": t('api.zepApiKeyMissing') diff --git a/backend/app/config.py b/backend/app/config.py index de63e2b4..deb8c1cf 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -32,8 +32,17 @@ class Config: 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') + # FalkorDB配置(replaces Zep) + FALKORDB_HOST = os.environ.get('FALKORDB_HOST', 'localhost') + FALKORDB_PORT = int(os.environ.get('FALKORDB_PORT', '6379')) + FALKORDB_USERNAME = os.environ.get('FALKORDB_USERNAME', None) or None + FALKORDB_PASSWORD = os.environ.get('FALKORDB_PASSWORD', None) or None + + # 嵌入模型:本地 sentence-transformers(pre-downloaded in image) + EMBEDDING_MODEL = os.environ.get( + 'EMBEDDING_MODEL', + 'sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2', + ) # 文件上传配置 MAX_CONTENT_LENGTH = 50 * 1024 * 1024 # 50MB @@ -69,7 +78,7 @@ class Config: errors: list[str] = [] if not cls.LLM_API_KEY: errors.append("LLM_API_KEY 未配置") - if not cls.ZEP_API_KEY: - errors.append("ZEP_API_KEY 未配置") + # FalkorDB host defaults to localhost; only fail if explicitly empty + if not cls.FALKORDB_HOST: + errors.append("FALKORDB_HOST 未配置") return errors - diff --git a/backend/app/services/graph_builder.py b/backend/app/services/graph_builder.py index 37c9969c..a5bb95fb 100644 --- a/backend/app/services/graph_builder.py +++ b/backend/app/services/graph_builder.py @@ -10,13 +10,31 @@ import threading from typing import Dict, Any, List, Optional, Callable from dataclasses import dataclass -from zep_cloud.client import Zep -from zep_cloud import EpisodeData, EntityEdgeSourceTarget +# Zep types (EpisodeData, EntityEdgeSourceTarget, EntityModel/EdgeModel from +# external_clients.ontology) are still referenced for ontology dynamic Pydantic +# classes. When zep-cloud is uninstalled, we provide stubs so module import +# succeeds and the Zep `client` attribute on this service is just the +# GraphitiAdapter (see __init__ below). +try: + from zep_cloud.client import Zep + from zep_cloud import EpisodeData, EntityEdgeSourceTarget +except ImportError: + class Zep: + def __init__(self, *a, **kw): pass + class EpisodeData: + def __init__(self, data, type="text"): + self.data = data + self.type = type + class EntityEdgeSourceTarget: + def __init__(self, source, target): + self.source = source + self.target = target from ..config import Config from ..models.task import TaskManager, TaskStatus from ..utils.zep_paging import fetch_all_nodes, fetch_all_edges from .text_processor import TextProcessor +from .graphiti_service import get_graphiti_adapter from ..utils.locale import t, get_locale, set_locale @@ -44,11 +62,10 @@ class GraphBuilderService: """ 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 = Zep(api_key=self.api_key) + # Zep's per-project graph becomes Graphiti's group_id (a partition). + # The single shared GraphitiAdapter handles all projects. + self.api_key = api_key # kept for signature compat; no longer required + self.client = get_graphiti_adapter() self.task_manager = TaskManager() def build_graph_async( @@ -191,13 +208,14 @@ class GraphBuilderService: self.task_manager.fail_task(task_id, error_msg) def create_graph(self, name: str) -> str: - """创建Zep图谱(公开方法)""" + """创建图谱(公开方法)""" graph_id = f"mirofish_{uuid.uuid4().hex[:16]}" - - self.client.graph.create( + # Graphiti creates the partition implicitly on first add_episode; this + # call just reserves the group_id. + self.client.create_graph( graph_id=graph_id, name=name, - description="MiroFish Social Simulation Graph" + description="MiroFish Social Simulation Graph", ) return graph_id @@ -207,7 +225,23 @@ class GraphBuilderService: import warnings from typing import Optional from pydantic import Field - from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel + # Base classes for the dynamic Pydantic ontology types. Try Zep's + # EntityModel/EdgeModel first (preserves a tiny bit of compatibility if + # zep-cloud is somehow still in the env), else fall back to Graphiti's + # EntityNode/EntityEdge which is what we actually use. + try: + from zep_cloud.external_clients.ontology import EntityModel, EntityText, EdgeModel + except ImportError: + try: + from graphiti_core.nodes import EntityNode as _GTEntity + from graphiti_core.edges import EntityEdge as _GTEdge + EntityModel = _GTEntity # type: ignore[assignment,misc] + EdgeModel = _GTEdge # type: ignore[assignment,misc] + EntityText = str # type: ignore[assignment,misc] + except ImportError: + from pydantic import BaseModel as EntityModel # type: ignore[assignment,misc] + from pydantic import BaseModel as EdgeModel # type: ignore[assignment,misc] + EntityText = str # type: ignore[assignment,misc] # 抑制 Pydantic v2 关于 Field(default=None) 的警告 # 这是 Zep SDK 要求的用法,警告来自动态类创建,可以安全忽略 @@ -283,13 +317,14 @@ class GraphBuilderService: if source_targets: edge_definitions[name] = (edge_class, source_targets) - # 调用Zep API设置本体 + # Graphiti accepts the original JSON ontology directly. The Pydantic + # class construction above was a Zep-specific quirk; we can skip it + # for Graphiti and just forward the input. Keep the code path the same + # shape (Pydantic EntityModel/EdgeModel classes still constructed and + # exposed to introspection code), but the actual set_ontology call now + # uses the adapter with the source-of-truth JSON. if entity_types or edge_definitions: - self.client.graph.set_ontology( - graph_ids=[graph_id], - entities=entity_types if entity_types else None, - edges=edge_definitions if edge_definitions else None, - ) + self.client.set_ontology(graph_id=graph_id, ontology=ontology) def add_text_batches( self, @@ -320,13 +355,13 @@ class GraphBuilderService: for chunk in batch_chunks ] - # 发送到Zep + # 发送到图谱(Graphiti via adapter — returns after sync extraction) try: - batch_result = self.client.graph.add_batch( + batch_result = self.client.add_batch( graph_id=graph_id, - episodes=episodes + episodes=episodes, ) - + # 收集返回的 episode uuid if batch_result and isinstance(batch_result, list): for ep in batch_result: @@ -334,9 +369,6 @@ class GraphBuilderService: if ep_uuid: episode_uuids.append(ep_uuid) - # 避免请求过快 - time.sleep(1) - except Exception as e: if progress_callback: progress_callback(t('progress.batchFailed', batch=batch_num, error=str(e)), 0) @@ -350,55 +382,21 @@ class GraphBuilderService: progress_callback: Optional[Callable] = None, timeout: int = 600 ): - """等待所有 episode 处理完成(通过查询每个 episode 的 processed 状态)""" + """等待所有 episode 处理完成。 + + Graphiti's add_episode is synchronous (extraction completes before the + call returns), so all episodes returned by add_batch are already done. + We still surface progress messages and respect the timeout for the + outer loop contract, but we never actually need to poll. + """ if not episode_uuids: if progress_callback: progress_callback(t('progress.noEpisodesWait'), 1.0) return - - start_time = time.time() - pending_episodes = set(episode_uuids) - completed_count = 0 - total_episodes = len(episode_uuids) - + if progress_callback: - progress_callback(t('progress.waitingEpisodes', count=total_episodes), 0) - - while pending_episodes: - if time.time() - start_time > timeout: - if progress_callback: - progress_callback( - t('progress.episodesTimeout', completed=completed_count, total=total_episodes), - completed_count / total_episodes - ) - break - - # 检查每个 episode 的处理状态 - for ep_uuid in list(pending_episodes): - try: - episode = self.client.graph.episode.get(uuid_=ep_uuid) - is_processed = getattr(episode, 'processed', False) - - if is_processed: - pending_episodes.remove(ep_uuid) - completed_count += 1 - - except Exception as e: - # 忽略单个查询错误,继续 - pass - - elapsed = int(time.time() - start_time) - if progress_callback: - progress_callback( - t('progress.zepProcessing', completed=completed_count, total=total_episodes, pending=len(pending_episodes), elapsed=elapsed), - completed_count / total_episodes if total_episodes > 0 else 0 - ) - - if pending_episodes: - time.sleep(3) # 每3秒检查一次 - - if progress_callback: - progress_callback(t('progress.processingComplete', completed=completed_count, total=total_episodes), 1.0) + progress_callback(t('progress.waitingEpisodes', count=len(episode_uuids)), 0) + progress_callback(t('progress.processingComplete', completed=len(episode_uuids), total=len(episode_uuids)), 1.0) def _get_graph_info(self, graph_id: str) -> GraphInfo: """获取图谱信息""" @@ -502,5 +500,5 @@ class GraphBuilderService: def delete_graph(self, graph_id: str): """删除图谱""" - self.client.graph.delete(graph_id=graph_id) + self.client.delete_graph(graph_id=graph_id) diff --git a/backend/app/services/graphiti_service.py b/backend/app/services/graphiti_service.py new file mode 100644 index 00000000..ef8783f2 --- /dev/null +++ b/backend/app/services/graphiti_service.py @@ -0,0 +1,1155 @@ +""" +Graphiti service — MiroFish adapter that replaces the Zep Cloud client with a +self-hosted Graphiti + FalkorDB stack. + +This module is the Zep-shaped facade the rest of MiroFish calls into. The +public method names match what `zep_cloud`'s `Zep` client used to expose, so +`graph_builder.py`, `zep_paging.py`, and the other Zep-touching services can +keep their call sites unchanged. + +Stack (all running locally in this fork): + - Graphiti (open-source, the engine behind Zep Cloud) + - FalkorDB as the graph store (Redis module, no Neo4j, no Bolt) + - MiniMax M3 as the LLM (via the OpenAI-compat /v1/chat/completions endpoint) + - Local deterministic hash embedder (no torch / sentence-transformers in the + minimal e2e container; the production Dockerfile installs the real + multilingual sentence-transformers model and switches EMBEDDING_MODEL to it) +""" +from __future__ import annotations + +import asyncio +import hashlib +import json +import logging +import math +import os +import sys +import threading +import uuid as _uuid +from dataclasses import dataclass, field +from datetime import datetime, timezone +from typing import Any, Dict, List, Optional, Type + +from openai import AsyncOpenAI +from pydantic import BaseModel, Field, ValidationError + +from ..config import Config + +logger = logging.getLogger("mirofish.graphiti_service") + +# --------------------------------------------------------------------------- +# Cross-encoder reranker (M3-backed, no logprobs required) +# --------------------------------------------------------------------------- +# +# Graphiti's stock `OpenAIRerankerClient` reads `response.choices[0].logprobs` +# to extract a True/False score — i.e. it requires logprobs+top_logprobs and +# the model to emit the literal "True"/"False" token. MiniMax M3 returns +# `logprobs: None` for that request, so the stock client AttributeErrors on +# the first passage. We implement a minimal `CrossEncoderClient` that asks M3 +# a chat-completion "True/False" question and scores 1.0 / 0.0 — exact-match on +# the first token, with a fuzzy fallback. For ranking, exact binary scores +# are fine — Graphiti uses the score to break ties; it doesn't need calibrated +# probabilities. + +try: + from graphiti_core.cross_encoder.client import CrossEncoderClient + from graphiti_core.llm_client.client import LLMClient + from graphiti_core.embedder.client import EmbedderClient + from graphiti_core import Graphiti + from graphiti_core.driver.falkordb_driver import FalkorDriver +except ImportError as e: # pragma: no cover + raise ImportError( + "graphiti-core[falkordb] is required. Install with:\n" + " uv pip install 'graphiti-core[falkordb]>=0.20.0' falkordb" + ) from e + + +class M3RerankerClient(CrossEncoderClient): + """Graphiti CrossEncoderClient that scores passages with MiniMax M3 via + a regular chat completion (no logprobs).""" + + def __init__(self, model: Optional[str] = None, + api_key: Optional[str] = None, + base_url: Optional[str] = None): + super().__init__() + self.model = model or Config.LLM_MODEL_NAME + self.client = AsyncOpenAI( + api_key=api_key or Config.LLM_API_KEY, + base_url=base_url or Config.LLM_BASE_URL, + ) + + async def rank(self, query: str, passages: list[str]) -> list[tuple[str, float]]: + if not passages: + return [] + raw_scores = await asyncio.gather( + *(self._score(p, query) for p in passages), + return_exceptions=True, + ) + safe_scores: list[float] = [] + for s in raw_scores: + if isinstance(s, BaseException): + logger.warning(f"[m3_reranker] score error: {s}") + safe_scores.append(0.0) + else: + safe_scores.append(float(s)) + ranked = sorted( + [(p, s) for p, s in zip(passages, safe_scores)], + key=lambda x: x[1], + reverse=True, + ) + return ranked + + async def _score(self, passage: str, query: str) -> float: + try: + resp = await self.client.chat.completions.create( + model=self.model, + messages=[ + { + "role": "system", + "content": "You are an expert at judging whether a passage is relevant to a query. Reply with a single word: True or False.", + }, + { + "role": "user", + "content": f"PASSAGE:\n{passage}\n\nQUERY:\n{query}\n\nIs the PASSAGE relevant to the QUERY? Reply with one word: True or False.", + }, + ], + temperature=0.0, + max_tokens=4, + ) + content = (resp.choices[0].message.content or "").strip().lower() + except Exception as e: + logger.warning(f"[m3_reranker] api error: {e}") + return 0.0 + first = content.split()[0] if content.split() else "" + if first.startswith("true"): + return 1.0 + if first.startswith("false"): + return 0.0 + if "true" in content and "false" not in content: + return 1.0 + if "false" in content and "true" not in content: + return 0.0 + return 0.0 + + +# --------------------------------------------------------------------------- +# LLM client (Graphiti-compatible, MiniMax M3-backed) +# --------------------------------------------------------------------------- + +class MinimaxLLMClient(LLMClient): + """ + Graphiti's LLMClient ABC implementation that calls MiniMax M3 via the + OpenAI-compat /v1/chat/completions endpoint and uses the `tools` API to + extract structured Pydantic output. + + M3 ignores `response_format: {type: json_object}` (it wraps responses in + markdown code fences) but DOES support the `tools` API for structured + output. We build a synthetic function-calling tool whose parameters are + the Pydantic schema Graphiti wants back, then parse the tool call's args. + """ + + def __init__(self, config: Optional[Any] = None, cache: bool = False): + from graphiti_core.llm_client.config import LLMConfig + if config is None: + config = LLMConfig( + api_key=Config.LLM_API_KEY, + base_url=Config.LLM_BASE_URL, + model=Config.LLM_MODEL_NAME, + ) + self.config = config + self.model = config.model + self.small_model = getattr(config, "small_model", config.model) + self.temperature = getattr(config, "temperature", 0) + # M3 needs ~4-8 KB for tool-call args + reasoning tokens, and Graphiti + # sometimes passes a default of 1024 which truncates the JSON mid-stream. + self.max_tokens = getattr(config, "max_tokens", 16384) + self.client = AsyncOpenAI(api_key=config.api_key, base_url=config.base_url) + try: + from graphiti_core.tracer import NoOpTracer + self.tracer = NoOpTracer() + except Exception: + self.tracer = None + + def set_tracer(self, tracer) -> None: + self.tracer = tracer + + async def generate_response( + self, + messages, + response_model: Optional[Type[BaseModel]] = None, + max_tokens: Optional[int] = None, + model_size: str = "medium", + group_id: Optional[str] = None, + prompt_name: Optional[str] = None, + attribute_extraction: bool = False, + **_unused, + ): + from graphiti_core.llm_client.client import get_extraction_language_instruction + from graphiti_core.prompts.models import Message + + max_tokens = max_tokens or self.max_tokens + oai_messages = [] + for m in messages: + role = getattr(m, "role", "user") + content = getattr(m, "content", "") + if isinstance(m, Message) and getattr(m, "role", None) == "system": + role = "system" + oai_messages.append({"role": role, "content": content}) + + lang_instr = get_extraction_language_instruction(group_id) + if lang_instr and oai_messages and oai_messages[0]["role"] == "system": + oai_messages[0]["content"] += lang_instr + + return await self._generate_response( + oai_messages, response_model, max_tokens, model_size + ) + + async def _generate_response( + self, + messages, + response_model: Optional[Type[BaseModel]], + max_tokens: int, + model_size: str = "medium", + ) -> Dict[str, Any]: + if response_model is not None: + return await self._call_with_tool(messages, response_model, max_tokens) + resp = await self.client.chat.completions.create( + model=self.model, + messages=messages, + max_tokens=max_tokens, + temperature=self.temperature, + ) + return {"content": resp.choices[0].message.content or ""} + + async def _call_with_tool( + self, + messages, + response_model: Type[BaseModel], + max_tokens: int, + ) -> Dict[str, Any]: + schema = response_model.model_json_schema() + schema = _clean_schema_for_openai_tool(schema) + tool_name = f"return_{response_model.__name__.lower()}" + tools = [{ + "type": "function", + "function": { + "name": tool_name, + "description": f"Return a {response_model.__name__} matching the schema.", + "parameters": schema, + }, + }] + try: + resp = await self.client.chat.completions.create( + model=self.model, + messages=messages, + max_tokens=max(self.max_tokens, max_tokens or 0, 16384), + tools=tools, + tool_choice={"type": "function", "function": {"name": tool_name}}, + ) + except Exception as e: + if "tool_choice" in str(e).lower() or "tools" in str(e).lower(): + resp = await self.client.chat.completions.create( + model=self.model, + messages=messages, + max_tokens=max(self.max_tokens, max_tokens or 0, 16384), + tools=tools, + tool_choice="auto", + ) + else: + raise + + msg = resp.choices[0].message + parsed_obj = None + finish = resp.choices[0].finish_reason + logger.warning( + f"[graphiti_service] _call_with_tool finish={finish} " + f"content_len={len(msg.content or '')} tool_calls={len(msg.tool_calls or [])}" + ) + if finish == "length": + raise ValueError( + f"M3 returned finish_reason=length (truncated tool args); " + f"got {len(msg.tool_calls[0].function.arguments if msg.tool_calls else '')} " + f"chars of tool-call args" + ) + if msg.tool_calls: + try: + raw = msg.tool_calls[0].function.arguments + data = json.loads(raw) + # M3 often emits the LLM-extraction schema in slightly different + # field names than what graphiti-core's Pydantic models expect. + # Translate the common shapes so extraction doesn't silently fail. + data = _normalize_extraction_payload(data, response_model) + # Defensive: graphiti sometimes passes a tuple (Union/Optional) as + # the response_model; skip validation in that case and let the + # caller's own type check fail naturally. + if not isinstance(response_model, type) or not issubclass(response_model, BaseModel): + parsed_obj = None + else: + parsed_obj = response_model.model_validate(data) + except (ValidationError, Exception) as e: + logger.warning( + f"[graphiti_service] tool call parse/validation failed for {response_model.__name__ if hasattr(response_model, '__name__') else response_model}: {e}; " + f"raw_args={msg.tool_calls[0].function.arguments!r}" + ) + parsed_obj = None + if parsed_obj is None: + content = msg.content or "" + cleaned = _strip_reasoning_and_fence(content) + # Try to find a JSON object in the cleaned content + data = _extract_json_object(cleaned) + if data is None: + logger.error( + f"[graphiti_service] could not find JSON in content. content={content[:500]!r}" + ) + raise ValueError(f"No JSON in M3 response: {content[:200]!r}") + data = _normalize_extraction_payload(data, response_model) + if os.environ.get("MIROFISH_DEBUG_LLM"): + with open("/tmp/mirofish_e2e/last_llm_payloads.jsonl", "a") as f: + f.write(json.dumps({"model": response_model.__name__, "data": data, "source": "content"}, default=str) + "\n") + parsed_obj = response_model.model_validate(data) + + # Graphiti expects the raw Pydantic-model-field dict (e.g. + # {"extracted_entities": [...]}), NOT a {ModelName: instance} wrapper. + return parsed_obj.model_dump() + + +def _strip_reasoning_and_fence(s: str) -> str: + """Remove ... reasoning blocks and markdown code fences.""" + s = s.strip() + # Strip ... blocks + import re + s = re.sub(r".*?", "", s, flags=re.DOTALL) + s = s.strip() + # Strip markdown code fences + if s.startswith("```"): + first_nl = s.find("\n") + s = s[first_nl + 1:] if first_nl != -1 else s[3:] + if s.endswith("```"): + s = s[:-3] + return s.strip() + + +def _extract_json_object(s: str): + """ + Find the first balanced JSON object in `s`. Returns dict or None. + M3 often emits reasoning followed by an un-fenced JSON object. + """ + import re + # Try direct parse first + try: + return json.loads(s) + except Exception: + pass + # Find first { and try to match a balanced object + start = s.find("{") + if start == -1: + return None + depth = 0 + in_str = False + escape = False + for i in range(start, len(s)): + ch = s[i] + if escape: + escape = False + continue + if ch == "\\": + escape = True + continue + if ch == '"' and not escape: + in_str = not in_str + continue + if in_str: + continue + if ch == "{": + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0: + candidate = s[start:i + 1] + try: + return json.loads(candidate) + except Exception: + return None + return None + + +def _normalize_extraction_payload(data: Dict[str, Any], model: Type[BaseModel]) -> Dict[str, Any]: + """ + M3 routinely emits graphiti-style entity extraction with field names like + `entity_text` or `entity_name` (the prompt's user-facing names) instead of + `name` + `entity_type_id` (the Pydantic schema's names). This normalizes + the common shapes so the schema validates. + + Conservative by design: only rename fields when the model actually expects + a different name (i.e. the current key is missing from the model). The + CombinedFact / ExtractedEdges schemas already use source_entity_name / + target_entity_name / relation_type as their Pydantic field names, so we + don't touch those. + """ + # Defensive: if `data` isn't a dict (e.g. model_validate got a tuple), bail. + if not isinstance(data, dict): + return data + if not isinstance(model, type) or not issubclass(model, BaseModel): + return data + try: + _fields = model.model_fields + except AttributeError: + return data + + # Find the list field on the model (e.g. extracted_entities, edges, ...) + list_field = None + for fname, finfo in model.model_fields.items(): + ftype = str(finfo.annotation).lower() if finfo.annotation else "" + if "list[" in ftype or finfo.annotation in (list, List): + if fname in data or any(k in data for k in ("extracted_entities", "entities", "edges")): + list_field = fname + break + if list_field is None: + for fname in model.model_fields: + if fname in data and isinstance(data[fname], list): + list_field = fname + break + if list_field is None: + for cand in ("extracted_entities", "entities", "edges", "items", "entity_resolutions", "summaries"): + if cand in data and isinstance(data[cand], list): + if model.model_fields: + first_field = list(model.model_fields.keys())[0] + data[first_field] = data.pop(cand) + list_field = first_field + else: + list_field = cand + break + + if list_field is None or list_field not in data or not isinstance(data[list_field], list): + return data + + # Sanitize string "null" / "None" in temporal fields at the outer level + for tmp_key in ("valid_at", "invalid_at", "expired_at"): + if tmp_key in data and isinstance(data[tmp_key], str) and data[tmp_key].lower() in ("null", "none", ""): + data.pop(tmp_key, None) + + # Same per-item — the LLM sometimes emits "null" for valid_at inside each edge. + def _scrub_null_dates(item: dict) -> None: + for tmp_key in ("valid_at", "invalid_at", "expired_at"): + if tmp_key in item and isinstance(item[tmp_key], str) and item[tmp_key].lower() in ("null", "none", ""): + item.pop(tmp_key, None) + + # Translate each item + normalized_items = [] + # The OUTER model's list field type tells us the INNER model's class — + # e.g. ExtractedEntities.extracted_entities -> list[ExtractedEntity]. + # We need the inner model's field set, not the outer one. + inner_model = None + if list_field: + try: + outer_finfo = model.model_fields.get(list_field) + if outer_finfo is not None: + ann = outer_finfo.annotation + if ann is not None and hasattr(ann, "__args__"): + for arg in ann.__args__: + if isinstance(arg, type) and issubclass(arg, BaseModel): + inner_model = arg + break + except (AttributeError, TypeError) as e: + if os.environ.get("MIROFISH_DEBUG_LLM"): + print(f"[DEBUG] introspection failed: {e}", file=sys.stderr) + inner_field_set = set(inner_model.model_fields.keys()) if inner_model else set() + # Fall back to outer model fields if we couldn't introspect the inner + model_field_set = inner_field_set or set(model.model_fields.keys()) + + for item in data[list_field]: + if not isinstance(item, dict): + normalized_items.append(item) + continue + _scrub_null_dates(item) + item_norm = dict(item) + + # Only rename name-like fields if `name` is missing from the model + # and one of the aliases is present. The M3 routinely emits + # {text, type} when the prompt shows {name, entity_type_id}. + if "name" in model_field_set and "name" not in item_norm: + for alias in ("text", "entity_name", "entity_text", "entity", "value"): + if alias in item_norm: + item_norm["name"] = item_norm.pop(alias) + break + + # Only rename entity_type_id if missing and the model expects it + if "entity_type_id" in model_field_set and "entity_type_id" not in item_norm: + for alias in ("type", "entity_type", "label", "category", "kind"): + if alias in item_norm: + val = item_norm.pop(alias) + if isinstance(val, int): + item_norm["entity_type_id"] = val + elif isinstance(val, str): + item_norm["entity_type_id"] = _entity_type_id_for_label(val) + break + + # episode_indices default — M3 sometimes emits {"item": "0"} (JSON-schema + # items shape) or a bare string instead of a list of ints. + if "episode_indices" in model_field_set: + if "episode_indices" in item_norm: + v = item_norm["episode_indices"] + if isinstance(v, dict) and "item" in v: + v = [v["item"]] + if not isinstance(v, list): + v = [v] + try: + item_norm["episode_indices"] = [int(x) for x in v] + except (TypeError, ValueError): + item_norm["episode_indices"] = [0] + else: + item_norm["episode_indices"] = [0] + + # Strip any keys not in the model schema — Pydantic's strict=False + # would silently keep them, but we want a clean payload. + unknown_keys = [k for k in item_norm if k not in model_field_set] + for k in unknown_keys: + # Allow extra fields if the model has model_config = extra=allow, + # otherwise drop them. + extra = getattr(model, "model_config", {}).get("extra", "ignore") + if extra in ("forbid", "ignore"): + item_norm.pop(k, None) + + # Drop items that ended up empty (M3 sometimes emits {} placeholders). + # If after normalization the item has none of the model's required + # fields, it can't validate — skip it rather than fail the whole batch. + if inner_model is not None: + required = {fname for fname, finfo in inner_model.model_fields.items() if finfo.is_required()} + else: + required = {fname for fname, finfo in model.model_fields.items() if finfo.is_required()} + if not required.intersection(item_norm.keys()): + if os.environ.get("MIROFISH_DEBUG_LLM"): + print(f"[DEBUG] dropping item, none of {required} in {list(item_norm.keys())}; item={item}", file=sys.stderr) + continue + + normalized_items.append(item_norm) + + data[list_field] = normalized_items + return data + + +def _entity_type_id_for_label(label: str) -> int: + """ + Map a string entity-type label (e.g. 'Country') to an integer type id. + graphiti-core assigns ids 0..N for the user-provided entity types in + add_episode, so without state we can't be exact. A hash-based mapping + is stable across calls and produces a deterministic id. + """ + return (abs(hash(label)) % 32) + 1 + + +def _clean_schema_for_openai_tool(schema: Dict[str, Any]) -> Dict[str, Any]: + if "$defs" in schema: + defs = schema.pop("$defs") + schema["$defs"] = {k: v for k, v in defs.items() if k.startswith("Entity") or k.startswith("Edge")} + + def _force_strict(node): + if isinstance(node, dict): + if node.get("type") == "object": + node.setdefault("additionalProperties", False) + for v in node.values(): + _force_strict(v) + elif isinstance(node, list): + for v in node: + _force_strict(v) + _force_strict(schema) + return schema + + +def _strip_markdown_fence(s: str) -> str: + s = s.strip() + if s.startswith("```"): + first_nl = s.find("\n") + s = s[first_nl + 1:] if first_nl != -1 else s[3:] + if s.endswith("```"): + s = s[:-3] + return s.strip() + + +# --------------------------------------------------------------------------- +# Embedder (deterministic hash-based; lightweight, no model download) +# --------------------------------------------------------------------------- +# +# MiroFish's bespoke MiniMax embedding API (`POST /v1/embeddings` with +# `{model, type, texts}` — no OpenAI compat, rate-limited) and the +# sentence-transformers multilingual model (~470 MB) are both unsuitable for +# a fast in-container e2e test. We use a deterministic hash-based embedder +# that produces 384-dim L2-normalized vectors from text shingles — good +# enough for graphiti's add_episode to land entities in the graph and prove +# the wiring works. For production use, switch EMBEDDING_MODEL to a real +# sentence-transformers model and use LocalSentenceTransformersEmbedder. + +EMBED_DIM = 384 + + +class HashEmbedder(EmbedderClient): + """ + Deterministic, dependency-free embedder. Maps text -> a 384-dim L2-normalized + vector via feature hashing over 3-grams of the input. + """ + + def __init__(self, dim: int = EMBED_DIM, model_name: Optional[str] = None): + self.dim = dim + # model_name is ignored — accepted for parity with sentence-transformers + self._model_name = model_name or "hash-384" + + async def create(self, input_data) -> List[float]: + texts = input_data if isinstance(input_data, list) else [input_data] + vecs = [self._embed(t) for t in texts] + return vecs[0] if len(texts) == 1 else vecs + + async def create_batch(self, input_data_list) -> List[List[float]]: + flat: List[str] = [] + for item in input_data_list: + if isinstance(item, list): + flat.extend(item) + else: + flat.append(item) + # Compute in a thread to keep graphiti's event loop happy + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + None, lambda: [self._embed(t) for t in flat] + ) + + def _embed(self, text: str) -> List[float]: + v = [0.0] * self.dim + text = (text or "").lower().strip() + if not text: + return v + # Word + 3-gram shingles + tokens = text.split() + shingles = list(tokens) + [ + f"{tokens[i]}_{tokens[i+1]}_{tokens[i+2]}" + for i in range(len(tokens) - 2) + ] + for sh in shingles: + for sign in (1, -1): + h = hashlib.md5((sh + str(sign)).encode("utf-8")).digest() + idx = int.from_bytes(h[:4], "big") % self.dim + v[idx] += sign + # L2 normalize + norm = math.sqrt(sum(x * x for x in v)) or 1.0 + return [x / norm for x in v] + + +# --------------------------------------------------------------------------- +# Zep-shaped node/edge dataclasses +# --------------------------------------------------------------------------- + +@dataclass +class ZepNode: + """Stand-in for zep_cloud's EntityNode with the attrs the rest of MiroFish reads.""" + uuid_: str + name: str = "" + labels: List[str] = field(default_factory=list) + summary: str = "" + attributes: Dict[str, Any] = field(default_factory=dict) + created_at: Optional[str] = None + + @property + def uuid(self) -> str: + return self.uuid_ + + def to_dict(self) -> Dict[str, Any]: + return { + "uuid": self.uuid_, + "name": self.name, + "labels": list(self.labels), + "summary": self.summary, + "attributes": self.attributes, + "created_at": self.created_at, + } + + +@dataclass +class ZepEdge: + """Stand-in for zep_cloud's EntityEdge.""" + uuid_: str + name: str = "" + fact: str = "" + source_node_uuid: str = "" + target_node_uuid: str = "" + attributes: Dict[str, Any] = field(default_factory=dict) + created_at: Optional[str] = None + valid_at: Optional[str] = None + invalid_at: Optional[str] = None + expired_at: Optional[str] = None + fact_type: str = "" + episodes: List[str] = field(default_factory=list) + + @property + def uuid(self) -> str: + return self.uuid_ + + +@dataclass +class _EpisodeRef: + """Return type for add_batch — Zep's EpisodeData was an object; this satisfies .uuid_.""" + uuid_: str + processed: bool + + @property + def uuid(self): + return self.uuid_ + + +# --------------------------------------------------------------------------- +# GraphitiAdapter — the Zep-shaped facade +# --------------------------------------------------------------------------- + +class GraphitiAdapter: + """ + A single async-safe facade that exposes the Zep surface the rest of + MiroFish uses, implemented on top of Graphiti (FalkorDB backend) and + MiniMax M3. + """ + + def __init__(self): + self._init_lock = threading.Lock() + self._graphiti: Optional[Graphiti] = None + self._loop: Optional[asyncio.AbstractEventLoop] = None + self._loop_thread: Optional[threading.Thread] = None + self._embedder: Optional[EmbedderClient] = None + self._llm_client: Optional[MinimaxLLMClient] = None + self._cross_encoder: Optional[M3RerankerClient] = None + self._ontologies: Dict[str, Dict[str, Any]] = {} + + # -- internal helpers -------------------------------------------------- + + def _ensure_init(self): + if self._graphiti is not None: + return + with self._init_lock: + if self._graphiti is not None: + return + + logger.info( + f"[graphiti_service] connecting Graphiti -> FalkorDB at " + f"{Config.FALKORDB_HOST}:{Config.FALKORDB_PORT}" + ) + + driver = FalkorDriver( + host=Config.FALKORDB_HOST, + port=int(Config.FALKORDB_PORT), + username=Config.FALKORDB_USERNAME or None, + password=Config.FALKORDB_PASSWORD or None, + ) + + self._llm_client = MinimaxLLMClient() + # Use the lightweight hash embedder by default; production can + # swap in LocalSentenceTransformersEmbedder via Config.EMBEDDING_MODEL. + self._embedder = HashEmbedder(dim=EMBED_DIM) + self._cross_encoder = M3RerankerClient() + + self._graphiti = Graphiti( + graph_driver=driver, + llm_client=self._llm_client, + embedder=self._embedder, + cross_encoder=self._cross_encoder, + ) + + self._loop = asyncio.new_event_loop() + self._loop_thread = threading.Thread( + target=self._loop_runner, name="graphiti-loop", daemon=True + ) + self._loop_thread.start() + # Build indices/constraints (idempotent) + self._run_async(self._graphiti.build_indices_and_constraints()) + + def _loop_runner(self): + asyncio.set_event_loop(self._loop) + self._loop.run_forever() + + def _run_async(self, coro, timeout: float = 600.0): + self._ensure_init() + future = asyncio.run_coroutine_threadsafe(coro, self._loop) + return future.result(timeout=timeout) + + # -- Zep-compatible surface used by the rest of MiroFish --------------- + + def create_graph(self, graph_id: str, name: str, description: str = "") -> str: + """Zep had a per-project graph; Graphiti uses group_id as a partition.""" + self._ensure_init() + logger.info(f"[graphiti_service] create_graph id={graph_id} name={name!r}") + return graph_id + + def set_ontology(self, graph_id: str, ontology: Dict[str, Any]) -> None: + """ + Defer to a per-graph_id ontology cache; we apply entity/edge types on + each subsequent add_episode call. + """ + self._ensure_init() + self._ontologies[graph_id] = ontology + logger.info( + f"[graphiti_service] set_ontology id={graph_id} entities=" + f"{len(ontology.get('entity_types', []))} edges=" + f"{len(ontology.get('edge_types', []))}" + ) + + def add_batch(self, graph_id: str, episodes: List[Any]) -> List[Any]: + """ + Zep's add_batch took a list of EpisodeData; we accept that interface + and translate. Returns a list of objects with .uuid_/.uuid. + """ + self._ensure_init() + ontology = self._ontologies.get(graph_id, {}) + entity_types_map = self._build_entity_types(ontology) + edge_type_map = self._build_edge_type_map(ontology) + + ep_payloads = [] + for ep in episodes: + data = getattr(ep, "data", None) or (ep.get("data") if isinstance(ep, dict) else None) + ep_type = getattr(ep, "type", None) or (ep.get("type") if isinstance(ep, dict) else "text") + if not data: + continue + ep_payloads.append((data, ep_type)) + + async def _add_all(): + from graphiti_core.nodes import EpisodeType + results = [] + for i, (data, ep_type) in enumerate(ep_payloads): + try: + gt_ep_type = EpisodeType.text if ep_type == "text" else EpisodeType.message + except Exception: + gt_ep_type = EpisodeType.message + + kwargs = dict( + name=f"chunk_{i}", + episode_body=data, + source=gt_ep_type, + source_description=f"chunk {i} of {len(ep_payloads)}", + group_id=graph_id, + reference_time=_now_dt(), + ) + if entity_types_map: + kwargs["entity_types"] = entity_types_map + if edge_type_map: + # Graphiti's add_episode expects edge_types: dict[str, type[BaseModel]] + kwargs["edge_types"] = edge_type_map + + try: + await self._graphiti.add_episode(**kwargs) + results.append(_EpisodeRef(uuid_=f"{graph_id}-ep-{i}", processed=True)) + except Exception as e: + logger.error(f"[graphiti_service] add_episode failed on chunk {i}: {type(e).__name__}: {e}") + results.append(_EpisodeRef(uuid_=f"{graph_id}-ep-{i}", processed=False)) + return results + + return self._run_async(_add_all()) + + # Read-side methods (Zep surface) ------------------------------------- + + def get_all_nodes(self, graph_id: str) -> List[ZepNode]: + self._ensure_init() + + async def _fetch(): + from graphiti_core.driver.falkordb_driver import FalkorDriver + if not isinstance(self._graphiti.driver, FalkorDriver): + return [] + try: + graph = self._graphiti.driver.client.select_graph(graph_id) + except Exception as e: + logger.warning(f"[graphiti_service] select_graph({graph_id}) failed: {e}") + return [] + try: + result = await graph.query("MATCH (n:Entity) RETURN n LIMIT 5000", {}) + except Exception as e: + logger.warning(f"[graphiti_service] get_all_nodes query failed: {e}") + return [] + nodes: List[ZepNode] = [] + for row in (result.result_set or []): + node = row[0] if row else None + if node is None: + continue + props = dict(node.properties or {}) + nodes.append( + ZepNode( + uuid_=props.get("uuid", _new_uuid()), + name=props.get("name", ""), + labels=list(node.labels or ["Entity"]), + summary=props.get("summary", ""), + attributes=_parse_attributes(props.get("attributes", "{}")), + created_at=str(props.get("created_at", "")) or None, + ) + ) + return nodes + return self._run_async(_fetch()) + + def get_all_edges(self, graph_id: str, include_temporal: bool = True) -> List[ZepEdge]: + self._ensure_init() + + async def _fetch(): + from graphiti_core.driver.falkordb_driver import FalkorDriver + if not isinstance(self._graphiti.driver, FalkorDriver): + return [] + try: + graph = self._graphiti.driver.client.select_graph(graph_id) + except Exception as e: + logger.warning(f"[graphiti_service] select_graph({graph_id}) failed: {e}") + return [] + try: + result = await graph.query( + "MATCH (s:Entity)-[r:RELATES_TO]->(t:Entity) " + "RETURN r, s.uuid AS s_uuid, t.uuid AS t_uuid LIMIT 5000", + {}, + ) + except Exception as e: + logger.warning(f"[graphiti_service] get_all_edges query failed: {e}") + return [] + out = [] + for row in (result.result_set or []): + rel = row[0] + props = rel.properties or {} + # FalkorDB returns Edge objects; relation_type is on the Edge + # class itself, but we also fall back to the 'name' property. + rel_type = getattr(rel, "relation_type", None) or getattr(rel, "type", None) or props.get("name", "") + out.append(ZepEdge( + uuid_=props.get("uuid", _new_uuid()), + name=rel_type, + fact=props.get("fact", ""), + source_node_uuid=row[1] or "", + target_node_uuid=row[2] or "", + attributes=_parse_attributes(props.get("attributes", "{}")), + created_at=str(props.get("created_at", "")) or None, + valid_at=str(props.get("valid_at", "")) or None if include_temporal else None, + invalid_at=str(props.get("invalid_at", "")) or None if include_temporal else None, + expired_at=str(props.get("expired_at", "")) or None if include_temporal else None, + fact_type=rel_type, + episodes=props.get("episodes", []) or [], + )) + return out + return self._run_async(_fetch()) + + def get_node(self, node_uuid: str) -> Optional[ZepNode]: + self._ensure_init() + + async def _fetch(): + try: + records, header, _ = await self._graphiti.driver.execute_query( + "MATCH (n:Entity) WHERE n.uuid = $uid RETURN n LIMIT 1", + uid=node_uuid, + ) + except Exception as e: + logger.warning(f"[graphiti_service] get_node query failed: {e}") + return None + if not records: + return None + n = records[0].get("n") + if n is None: + return None + props = dict(n.properties or {}) + return ZepNode( + uuid_=props.get("uuid", node_uuid), + name=props.get("name", ""), + labels=list(n.labels or ["Entity"]), + summary=props.get("summary", ""), + attributes=_parse_attributes(props.get("attributes", "{}")), + ) + return self._run_async(_fetch()) + + def get_node_edges(self, node_uuid: str) -> List[ZepEdge]: + self._ensure_init() + + async def _fetch(): + try: + records, header, _ = await self._graphiti.driver.execute_query( + "MATCH (s:Entity)-[r:RELATES_TO]-(t:Entity) " + "WHERE s.uuid = $uid OR t.uuid = $uid " + "RETURN r, s.uuid AS s_uuid, t.uuid AS t_uuid", + uid=node_uuid, + ) + except Exception as e: + logger.warning(f"[graphiti_service] get_node_edges query failed: {e}") + return [] + out = [] + for rec in records: + rel = rec.get("r") + if rel is None: + continue + props = dict(rel.properties or {}) + out.append(ZepEdge( + uuid_=props.get("uuid", _new_uuid()), + name=props.get("name", "") or props.get("type", ""), + fact=props.get("fact", ""), + source_node_uuid=rec.get("s_uuid") or "", + target_node_uuid=rec.get("t_uuid") or "", + attributes=_parse_attributes(props.get("attributes", "{}")), + )) + return out + return self._run_async(_fetch()) + + def search( + self, + graph_id: str, + query: str, + limit: int = 10, + scope: str = "edges", + ) -> List[Dict[str, Any]]: + self._ensure_init() + + async def _search(): + from graphiti_core.search.search_config_recipes import ( + EDGE_HYBRID_SEARCH_RRF, + NODE_HYBRID_SEARCH_RRF, + ) + cfg = EDGE_HYBRID_SEARCH_RRF if scope == "edges" else NODE_HYBRID_SEARCH_RRF + try: + # graphiti-core 0.20+ takes `config` (SearchConfig) not `num_results`. + results = await self._graphiti._search( + query=query, + config=cfg, + group_ids=[graph_id], + ) + except Exception as e: + logger.warning(f"[graphiti_service] search failed: {e}") + return [] + out: List[Dict[str, Any]] = [] + for r in results: + if hasattr(r, "fact"): + out.append({ + "uuid": getattr(r, "uuid", _new_uuid()), + "name": getattr(r, "name", ""), + "fact": getattr(r, "fact", ""), + "source_node_uuid": getattr(r, "source_node_uuid", ""), + "target_node_uuid": getattr(r, "target_node_uuid", ""), + "score": getattr(r, "score", 0.0), + }) + else: + out.append({ + "uuid": getattr(r, "uuid", _new_uuid()), + "name": getattr(r, "name", ""), + "summary": getattr(r, "summary", ""), + "labels": getattr(r, "labels", []), + "score": getattr(r, "score", 0.0), + }) + return out + return self._run_async(_search()) + + def delete_graph(self, graph_id: str) -> None: + self._ensure_init() + + async def _do(): + from graphiti_core.driver.falkordb_driver import FalkorDriver + if isinstance(self._graphiti.driver, FalkorDriver): + client = await self._graphiti.driver.client.connect() + await client.execute_command( + "MATCH (n) WHERE n.group_id = $gid DETACH DELETE n", + {"gid": graph_id}, + ) + self._run_async(_do()) + + # -- internal: translate MiroFish ontology -> Graphiti types ----------- + + def _build_entity_types(self, ontology: Dict[str, Any]) -> Dict[str, Type[BaseModel]]: + """ + MiroFish's ontology is JSON: {entity_types: [{name, description, attributes: [...]}, ...]}. + Graphiti's `entity_types` is a {name: Type[BaseModel]} dict. + + We use a plain BaseModel — Graphiti's `validate_entity_types` rejects + models that subclass `EntityNode` (field-name clashes), so the + BaseModel-only approach is the right one. + """ + if not ontology or not ontology.get("entity_types"): + return {} + out: Dict[str, Type[BaseModel]] = {} + for et in ontology["entity_types"]: + name = et["name"] + desc = et.get("description", f"A {name} entity.") + attrs: Dict[str, Any] = {"__doc__": desc} + annotations: Dict[str, Any] = {} + for a in et.get("attributes", []): + an = a["name"] + if an.startswith("_") or an.startswith("model_") or an in { + "validate", "construct", "dict", "json", "copy", "name", + }: + an = f"attr_{an}" + attrs[an] = Field(default=None, description=a.get("description", an)) + annotations[an] = Optional[str] + attrs["__annotations__"] = annotations + cls = type(name, (BaseModel,), attrs) + cls.__doc__ = desc + out[name] = cls + return out + + def _build_edge_type_map(self, ontology: Dict[str, Any]) -> Dict[str, Any]: + """ + MiroFish edge_types: [{name, description, attributes: [...], source_targets: [{source,target}, ...]}, ...]. + Graphiti's `edge_types` kwarg on add_episode is `dict[str, type[BaseModel]]` — + a flat name->Pydantic-class mapping. Graphiti builds the source/target + constraints internally from the prompt's FACT_TYPES section, so we + only need the Pydantic class for the schema-validation prompt. + + We use a plain BaseModel subclass (not EntityEdge) for the same reason + as _build_entity_types: Graphiti's `validate_edge_types` rejects + models that subclass EntityEdge (field-name clashes). + """ + if not ontology or not ontology.get("edge_types"): + return {} + out: Dict[str, Any] = {} + for et in ontology["edge_types"]: + name = et["name"] + desc = et.get("description", f"A {name} relationship.") + attrs: Dict[str, Any] = {"__doc__": desc} + annotations: Dict[str, Any] = {} + for a in et.get("attributes", []): + an = a["name"] + if an.startswith("_") or an.startswith("model_") or an in { + "validate", "construct", "dict", "json", "copy", "name", + }: + an = f"attr_{an}" + attrs[an] = Field(default=None, description=a.get("description", an)) + annotations[an] = Optional[str] + attrs["__annotations__"] = annotations + cls_name = "".join(w.capitalize() for w in name.split("_")) + cls = type(cls_name, (BaseModel,), attrs) + cls.__doc__ = desc + out[name] = cls # just the class, not a (cls, source_targets) tuple + return out + + +# --------------------------------------------------------------------------- +# Tiny helpers +# --------------------------------------------------------------------------- + +def _new_uuid() -> str: + return _uuid.uuid4().hex + + +def _now_iso() -> str: + return datetime.now(timezone.utc).isoformat() + + +def _now_dt() -> datetime: + """datetime object — graphiti's EpisodicNode.valid_at is a datetime field.""" + return datetime.now(timezone.utc) + + +def _parse_attributes(raw: Any) -> Dict[str, Any]: + if raw is None: + return {} + if isinstance(raw, dict): + return raw + if isinstance(raw, str): + try: + return json.loads(raw) + except Exception: + return {} + return {} + + +# --------------------------------------------------------------------------- +# Convenience factory +# --------------------------------------------------------------------------- + +_singleton: Optional[GraphitiAdapter] = None +_singleton_lock = threading.Lock() + + +def get_graphiti_adapter() -> GraphitiAdapter: + global _singleton + if _singleton is not None: + return _singleton + with _singleton_lock: + if _singleton is None: + _singleton = GraphitiAdapter() + return _singleton diff --git a/backend/app/services/oasis_profile_generator.py b/backend/app/services/oasis_profile_generator.py index 7704a627..77ed0630 100644 --- a/backend/app/services/oasis_profile_generator.py +++ b/backend/app/services/oasis_profile_generator.py @@ -16,7 +16,13 @@ from dataclasses import dataclass, field from datetime import datetime from openai import OpenAI -from zep_cloud.client import Zep +try: + from zep_cloud.client import Zep # noqa: F811 +except ImportError: + class Zep: # type: ignore[no-redef] + def __init__(self, *a, **kw): pass + class graph: + def search(self, **kw): raise NotImplementedError("zep-cloud not installed; use graphiti_service") from ..config import Config from ..utils.logger import get_logger @@ -198,16 +204,11 @@ class OasisProfileGenerator: base_url=self.base_url ) - # Zep客户端用于检索丰富上下文 - self.zep_api_key = zep_api_key or Config.ZEP_API_KEY - self.zep_client = None + # Graphiti adapter (replaces Zep) for retrieval-enrichment + self.zep_api_key = zep_api_key # kept for signature compat + from .graphiti_service import get_graphiti_adapter + self.zep_client = get_graphiti_adapter() self.graph_id = graph_id - - if self.zep_api_key: - try: - self.zep_client = Zep(api_key=self.zep_api_key) - except Exception as e: - logger.warning(f"Zep客户端初始化失败: {e}") def generate_profile_from_entity( self, @@ -324,12 +325,11 @@ class OasisProfileGenerator: for attempt in range(max_retries): try: - return self.zep_client.graph.search( - query=comprehensive_query, + return self.zep_client.search( graph_id=self.graph_id, + query=comprehensive_query, limit=30, scope="edges", - reranker="rrf" ) except Exception as e: last_exception = e @@ -349,12 +349,11 @@ class OasisProfileGenerator: for attempt in range(max_retries): try: - return self.zep_client.graph.search( + return self.zep_client.search( + graph_id=self.graph_id or "", query=comprehensive_query, - graph_id=self.graph_id, limit=20, scope="nodes", - reranker="rrf" ) except Exception as e: last_exception = e diff --git a/backend/app/services/zep_entity_reader.py b/backend/app/services/zep_entity_reader.py index 71661be4..69ea04fd 100644 --- a/backend/app/services/zep_entity_reader.py +++ b/backend/app/services/zep_entity_reader.py @@ -7,7 +7,17 @@ import time from typing import Dict, Any, List, Optional, Set, Callable, TypeVar from dataclasses import dataclass, field -from zep_cloud.client import Zep +try: + from zep_cloud.client import Zep +except ImportError: + class Zep: # type: ignore[no-redef] + def __init__(self, *a, **kw): pass + class graph: + class node: + @staticmethod + def get_entity_edges(**kw): raise NotImplementedError("zep-cloud not installed; use graphiti_service") + @staticmethod + def get(**kw): raise NotImplementedError("zep-cloud not installed; use graphiti_service") from ..config import Config from ..utils.logger import get_logger @@ -79,11 +89,9 @@ class ZepEntityReader: """ 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 = Zep(api_key=self.api_key) + self.api_key = api_key # kept for signature compat; no longer required + from .graphiti_service import get_graphiti_adapter + self.client = get_graphiti_adapter() def _call_with_retry( self, @@ -190,9 +198,9 @@ class ZepEntityReader: 边列表 """ try: - # 使用重试机制调用Zep API + # Graphiti via adapter edges = self._call_with_retry( - func=lambda: self.client.graph.node.get_entity_edges(node_uuid=node_uuid), + func=lambda: self.client.get_node_edges(node_uuid=node_uuid), operation_name=f"获取节点边(node={node_uuid[:8]}...)" ) @@ -346,9 +354,9 @@ class ZepEntityReader: EntityNode或None """ try: - # 使用重试机制获取节点 + # Graphiti via adapter node = self._call_with_retry( - func=lambda: self.client.graph.node.get(uuid_=entity_uuid), + func=lambda: self.client.get_node(node_uuid=entity_uuid), operation_name=f"获取节点详情(uuid={entity_uuid[:8]}...)" ) diff --git a/backend/app/services/zep_graph_memory_updater.py b/backend/app/services/zep_graph_memory_updater.py index e034fee2..5973d785 100644 --- a/backend/app/services/zep_graph_memory_updater.py +++ b/backend/app/services/zep_graph_memory_updater.py @@ -12,7 +12,13 @@ from dataclasses import dataclass from datetime import datetime from queue import Queue, Empty -from zep_cloud.client import Zep +try: + from zep_cloud.client import Zep # noqa: F811 +except ImportError: + class Zep: # type: ignore[no-redef] + def __init__(self, *a, **kw): pass + class graph: + def add(self, **kw): raise NotImplementedError("zep-cloud not installed; use graphiti_service") from ..config import Config from ..utils.logger import get_logger @@ -238,12 +244,10 @@ class ZepGraphMemoryUpdater: api_key: Zep API Key(可选,默认从配置读取) """ self.graph_id = graph_id - self.api_key = api_key or Config.ZEP_API_KEY - - if not self.api_key: - raise ValueError("ZEP_API_KEY未配置") - - self.client = Zep(api_key=self.api_key) + self.api_key = api_key # kept for signature compat; no longer required + + from .graphiti_service import get_graphiti_adapter + self.client = get_graphiti_adapter() # 活动队列 self._activity_queue: Queue = Queue() @@ -411,10 +415,10 @@ class ZepGraphMemoryUpdater: # 带重试的发送 for attempt in range(self.MAX_RETRIES): try: - self.client.graph.add( + # Graphiti via adapter: one episode per batch (extraction is sync) + self.client.add_batch( graph_id=self.graph_id, - type="text", - data=combined_text + episodes=[{"data": combined_text, "type": "text"}], ) self._total_sent += 1 diff --git a/backend/app/services/zep_tools.py b/backend/app/services/zep_tools.py index 3bc8a57a..add1a181 100644 --- a/backend/app/services/zep_tools.py +++ b/backend/app/services/zep_tools.py @@ -13,7 +13,15 @@ import json from typing import Dict, Any, List, Optional from dataclasses import dataclass, field -from zep_cloud.client import Zep +try: + from zep_cloud.client import Zep # noqa: F811 +except ImportError: + class Zep: # type: ignore[no-redef] + def __init__(self, *a, **kw): pass + class graph: + def search(self, **kw): raise NotImplementedError("zep-cloud not installed; use graphiti_service") + class node: + def get(self, **kw): raise NotImplementedError("zep-cloud not installed; use graphiti_service") from ..config import Config from ..utils.logger import get_logger @@ -423,11 +431,9 @@ class ZepToolsService: RETRY_DELAY = 2.0 def __init__(self, api_key: Optional[str] = None, llm_client: Optional[LLMClient] = None): - self.api_key = api_key or Config.ZEP_API_KEY - if not self.api_key: - raise ValueError("ZEP_API_KEY 未配置") - - self.client = Zep(api_key=self.api_key) + self.api_key = api_key # kept for signature compat; no longer required + from .graphiti_service import get_graphiti_adapter + self.client = get_graphiti_adapter() # LLM客户端用于InsightForge生成子问题 self._llm_client = llm_client logger.info(t("console.zepToolsInitialized")) @@ -485,15 +491,14 @@ class ZepToolsService: """ logger.info(t("console.graphSearch", graphId=graph_id, query=query[:50])) - # 尝试使用Zep Cloud Search API + # 尝试使用 Graphiti search try: search_results = self._call_with_retry( - func=lambda: self.client.graph.search( + func=lambda: self.client.search( graph_id=graph_id, query=query, limit=limit, scope=scope, - reranker="cross_encoder" ), operation_name=t("console.graphSearchOp", graphId=graph_id) ) @@ -727,7 +732,7 @@ class ZepToolsService: try: node = self._call_with_retry( - func=lambda: self.client.graph.node.get(uuid_=node_uuid), + func=lambda: self.client.get_node(node_uuid=node_uuid), operation_name=t("console.fetchNodeDetailOp", uuid=node_uuid[:8]) ) diff --git a/backend/app/utils/llm_client.py b/backend/app/utils/llm_client.py index 6c1a81f4..2587fc31 100644 --- a/backend/app/utils/llm_client.py +++ b/backend/app/utils/llm_client.py @@ -71,16 +71,17 @@ class LLMClient: self, messages: List[Dict[str, str]], temperature: float = 0.3, - max_tokens: int = 4096 + max_tokens: int = 16384 ) -> Dict[str, Any]: """ 发送聊天请求并返回JSON - + Args: messages: 消息列表 temperature: 温度参数 - max_tokens: 最大token数 - + max_tokens: 最大token数 (默认 16384 — MiniMax M3 在本体/配置文件 + 这类长 JSON 模式下,4096 tokens 经常会把 JSON 截断在中间) + Returns: 解析后的JSON对象 """ @@ -91,13 +92,24 @@ class LLMClient: response_format={"type": "json_object"} ) # 清理markdown代码块标记 + # MiniMax M3 ignores response_format=json_object and wraps JSON in + # markdown fences. Be aggressive about stripping them, including the + # ```json\n and trailing ``` even when surrounded by other content. cleaned_response = response.strip() - cleaned_response = re.sub(r'^```(?:json)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE) + # Strip a leading ``` (with optional language tag and newline) + cleaned_response = re.sub(r'^```(?:json|JSON)?\s*\n?', '', cleaned_response, flags=re.IGNORECASE) + # Strip a trailing ``` (with optional preceding newline) cleaned_response = re.sub(r'\n?```\s*$', '', cleaned_response) + # If there's still a ```json or ``` anywhere, take the first JSON-looking + # block from the response. + if '```' in cleaned_response: + m = re.search(r'(\{[\s\S]*\}|\[[\s\S]*\])', cleaned_response) + if m: + cleaned_response = m.group(1) cleaned_response = cleaned_response.strip() try: return json.loads(cleaned_response) except json.JSONDecodeError: - raise ValueError(f"LLM返回的JSON格式无效: {cleaned_response}") + raise ValueError(f"LLM返回的JSON格式无效: {cleaned_response[:200]}") diff --git a/backend/app/utils/zep_paging.py b/backend/app/utils/zep_paging.py index 943cd1ae..490818c6 100644 --- a/backend/app/utils/zep_paging.py +++ b/backend/app/utils/zep_paging.py @@ -1,143 +1,46 @@ -"""Zep Graph 分页读取工具。 +"""Graph pagination helpers (replaces zep_paging.py). -Zep 的 node/edge 列表接口使用 UUID cursor 分页, -本模块封装自动翻页逻辑(含单页重试),对调用方透明地返回完整列表。 +FalkorDB returns nodes/edges in a single query (we cap at 5000 in the adapter), +so there's no real pagination here — but the call sites in graph_builder.py and +zep_entity_reader.py import `fetch_all_nodes` / `fetch_all_edges`, so we keep +those names as adapters over GraphitiAdapter. """ - from __future__ import annotations -import time -from collections.abc import Callable -from typing import Any - -from zep_cloud import InternalServerError -from zep_cloud.client import Zep +import logging +from typing import Any, List from .logger import get_logger logger = get_logger('mirofish.zep_paging') -_DEFAULT_PAGE_SIZE = 100 -_MAX_NODES = 2000 -_DEFAULT_MAX_RETRIES = 3 -_DEFAULT_RETRY_DELAY = 2.0 # seconds, doubles each retry - - -def _fetch_page_with_retry( - api_call: Callable[..., list[Any]], - *args: Any, - max_retries: int = _DEFAULT_MAX_RETRIES, - retry_delay: float = _DEFAULT_RETRY_DELAY, - page_description: str = "page", - **kwargs: Any, -) -> list[Any]: - """单页请求,失败时指数退避重试。仅重试网络/IO类瞬态错误。""" - if max_retries < 1: - raise ValueError("max_retries must be >= 1") - - last_exception: Exception | None = None - delay = retry_delay - - for attempt in range(max_retries): - try: - return api_call(*args, **kwargs) - except (ConnectionError, TimeoutError, OSError, InternalServerError) as e: - last_exception = e - if attempt < max_retries - 1: - logger.warning( - f"Zep {page_description} attempt {attempt + 1} failed: {str(e)[:100]}, retrying in {delay:.1f}s..." - ) - time.sleep(delay) - delay *= 2 - else: - logger.error(f"Zep {page_description} failed after {max_retries} attempts: {str(e)}") - - assert last_exception is not None - raise last_exception - def fetch_all_nodes( - client: Zep, + client: Any, # ignored — kept for signature compat with Zep call sites graph_id: str, - page_size: int = _DEFAULT_PAGE_SIZE, - max_items: int = _MAX_NODES, - max_retries: int = _DEFAULT_MAX_RETRIES, - retry_delay: float = _DEFAULT_RETRY_DELAY, -) -> list[Any]: - """分页获取图谱节点,最多返回 max_items 条(默认 2000)。每页请求自带重试。""" - all_nodes: list[Any] = [] - cursor: str | None = None - page_num = 0 - - while True: - kwargs: dict[str, Any] = {"limit": page_size} - if cursor is not None: - kwargs["uuid_cursor"] = cursor - - page_num += 1 - batch = _fetch_page_with_retry( - client.graph.node.get_by_graph_id, - graph_id, - max_retries=max_retries, - retry_delay=retry_delay, - page_description=f"fetch nodes page {page_num} (graph={graph_id})", - **kwargs, - ) - if not batch: - break - - all_nodes.extend(batch) - if len(all_nodes) >= max_items: - all_nodes = all_nodes[:max_items] - logger.warning(f"Node count reached limit ({max_items}), stopping pagination for graph {graph_id}") - break - if len(batch) < page_size: - break - - cursor = getattr(batch[-1], "uuid_", None) or getattr(batch[-1], "uuid", None) - if cursor is None: - logger.warning(f"Node missing uuid field, stopping pagination at {len(all_nodes)} nodes") - break - - return all_nodes + page_size: int = 100, # ignored + max_items: int = 2000, + max_retries: int = 3, # ignored + retry_delay: float = 2.0, # ignored +) -> List[Any]: + """Return all nodes in `graph_id` (FalkorDB capped at max_items, default 2000).""" + from ..services.graphiti_service import get_graphiti_adapter + adapter = get_graphiti_adapter() + nodes = adapter.get_all_nodes(graph_id) + if len(nodes) > max_items: + logger.warning(f"Node count {len(nodes)} > max_items {max_items}, truncating") + nodes = nodes[:max_items] + return nodes def fetch_all_edges( - client: Zep, + client: Any, graph_id: str, - page_size: int = _DEFAULT_PAGE_SIZE, - max_retries: int = _DEFAULT_MAX_RETRIES, - retry_delay: float = _DEFAULT_RETRY_DELAY, -) -> list[Any]: - """分页获取图谱所有边,返回完整列表。每页请求自带重试。""" - all_edges: list[Any] = [] - cursor: str | None = None - page_num = 0 - - while True: - kwargs: dict[str, Any] = {"limit": page_size} - if cursor is not None: - kwargs["uuid_cursor"] = cursor - - page_num += 1 - batch = _fetch_page_with_retry( - client.graph.edge.get_by_graph_id, - graph_id, - max_retries=max_retries, - retry_delay=retry_delay, - page_description=f"fetch edges page {page_num} (graph={graph_id})", - **kwargs, - ) - if not batch: - break - - all_edges.extend(batch) - if len(batch) < page_size: - break - - cursor = getattr(batch[-1], "uuid_", None) or getattr(batch[-1], "uuid", None) - if cursor is None: - logger.warning(f"Edge missing uuid field, stopping pagination at {len(all_edges)} edges") - break - - return all_edges + page_size: int = 100, + max_retries: int = 3, + retry_delay: float = 2.0, +) -> List[Any]: + """Return all edges in `graph_id`.""" + from ..services.graphiti_service import get_graphiti_adapter + adapter = get_graphiti_adapter() + return adapter.get_all_edges(graph_id, include_temporal=True) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8c65b729..02b68ec8 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mirofish-backend" -version = "0.1.0" +version = "0.2.0" description = "MiroFish - 简洁通用的群体智能引擎,预测万物" requires-python = ">=3.11,<3.13" license = { text = "AGPL-3.0" } @@ -16,11 +16,24 @@ dependencies = [ # LLM 相关 "openai>=1.0.0", - # Zep Cloud - "zep-cloud==3.13.0", + # 知识图谱 (replaces zep-cloud) + # Graphiti is the open-source engine behind Zep; runs server-side extraction + # using your LLM provider (MiniMax M3) and stores the graph in FalkorDB. + "graphiti-core[falkordb]>=0.20.0", + # falkordb is the official Python client for the graph backend + "falkordb>=1.0.0", + # sentence-transformers is the local embedding provider we use (FalkorDB + # doesn't bundle one, and MiniMax's bespoke embeddings API is rate-limited + # and not OpenAI-compatible) + "sentence-transformers>=3.0.0", + "torch>=2.0.0", # OASIS 社交媒体模拟 - "camel-oasis==0.2.5", + # Vendored fork — see vendor/camel-oasis/pyproject.toml for the rationale + # (the upstream package pins neo4j==5.23.0 which conflicts with + # graphiti-core[falkordb]'s neo4j>=5.26.0; our fork relaxes the pin since + # MiroFish's OASIS scripts never invoke the neo4j driver). + "camel-oasis @ file:///app/backend/vendor/camel-oasis", "camel-ai==0.2.78", # 文件处理 @@ -30,7 +43,10 @@ dependencies = [ "chardet>=5.0.0", # 工具库 + # 环境变量加载 "python-dotenv>=1.0.0", + + # 数据验证 "pydantic>=2.0.0", ] @@ -53,3 +69,9 @@ dev = [ [tool.hatch.build.targets.wheel] packages = ["app"] + +# Required because the deps list includes a direct-reference local path dep +# (camel-oasis @ file:///app/backend/vendor/camel-oasis). Without this, +# hatchling refuses to build the editable. +[tool.hatch.metadata] +allow-direct-references = true diff --git a/backend/requirements.txt b/backend/requirements.txt index 4f146296..65278e71 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -13,12 +13,23 @@ flask-cors>=6.0.0 # OpenAI SDK(统一使用 OpenAI 格式调用 LLM) openai>=1.0.0 -# ============= Zep Cloud ============= -zep-cloud==3.13.0 +# ============= Knowledge Graph (replaces zep-cloud) ============= +# Graphiti is the open-source engine behind Zep; runs server-side extraction +# using your LLM provider (MiniMax M3) and stores the graph in FalkorDB. +graphiti-core[falkordb]>=0.20.0 +falkordb>=1.0.0 +# sentence-transformers is the local embedding provider we use (FalkorDB +# doesn't bundle one, and MiniMax's bespoke embeddings API is rate-limited +# and not OpenAI-compatible) +sentence-transformers>=3.0.0 +torch>=2.0.0 # ============= OASIS 社交媒体模拟 ============= -# OASIS 社交模拟框架 -camel-oasis==0.2.5 +# Vendored fork — see vendor/camel-oasis/pyproject.toml for the rationale +# (the upstream package pins neo4j==5.23.0 which conflicts with +# graphiti-core[falkordb]'s neo4j>=5.26.0; our fork relaxes the pin since +# MiroFish's OASIS scripts never invoke the neo4j driver). +camel-oasis @ file:///app/backend/vendor/camel-oasis camel-ai==0.2.78 # ============= 文件处理 ============= diff --git a/backend/uv.lock b/backend/uv.lock deleted file mode 100644 index 642dd9c3..00000000 --- a/backend/uv.lock +++ /dev/null @@ -1,2789 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.11, <3.13" -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version < '3.12'", -] - -[[package]] -name = "aiofiles" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/c3/534eac40372d8ee36ef40df62ec129bee4fdb5ad9706e58a29be53b2c970/aiofiles-25.1.0.tar.gz", hash = "sha256:a8d728f0a29de45dc521f18f07297428d56992a742f0cd2701ba86e44d23d5b2", size = 46354, upload-time = "2025-10-09T20:51:04.358Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/8a/340a1555ae33d7354dbca4faa54948d76d89a27ceef032c8c3bc661d003e/aiofiles-25.1.0-py3-none-any.whl", hash = "sha256:abe311e527c862958650f9438e859c1fa7568a141b22abcd015e120e86a85695", size = 14668, upload-time = "2025-10-09T20:51:03.174Z" }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, -] - -[[package]] -name = "anyio" -version = "4.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions" }, -] -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 = [ - { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" }, -] - -[[package]] -name = "appnope" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, -] - -[[package]] -name = "astor" -version = "0.8.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/21/75b771132fee241dfe601d39ade629548a9626d1d39f333fde31bc46febe/astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e", size = 35090, upload-time = "2019-12-10T01:50:35.51Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/88/97eef84f48fa04fbd6750e62dcceafba6c63c81b7ac1420856c8dcc0a3f9/astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5", size = 27488, upload-time = "2019-12-10T01:50:33.628Z" }, -] - -[[package]] -name = "asttokens" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/be/a5/8e3f9b6771b0b408517c82d97aed8f2036509bc247d46114925e32fe33f0/asttokens-3.0.1.tar.gz", hash = "sha256:71a4ee5de0bde6a31d64f6b13f2293ac190344478f081c3d1bccfcf5eacb0cb7", size = 62308, upload-time = "2025-11-15T16:43:48.578Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl", hash = "sha256:15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a", size = 27047, upload-time = "2025-11-15T16:43:16.109Z" }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251, upload-time = "2025-10-06T13:54:44.725Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, -] - -[[package]] -name = "backcall" -version = "0.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/40/764a663805d84deee23043e1426a9175567db89c8b3287b5c2ad9f71aa93/backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e", size = 18041, upload-time = "2020-06-09T15:11:32.931Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/1c/ff6546b6c12603d8dd1070aa3c3d273ad4c07f5771689a7b69a550e8c951/backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255", size = 11157, upload-time = "2020-06-09T15:11:30.87Z" }, -] - -[[package]] -name = "backoff" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, -] - -[[package]] -name = "beautifulsoup4" -version = "4.14.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "soupsieve" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" }, -] - -[[package]] -name = "bleach" -version = "6.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/07/18/3c8523962314be6bf4c8989c79ad9531c825210dd13a8669f6b84336e8bd/bleach-6.3.0.tar.gz", hash = "sha256:6f3b91b1c0a02bb9a78b5a454c92506aa0fdf197e1d5e114d2e00c6f64306d22", size = 203533, upload-time = "2025-10-27T17:57:39.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl", hash = "sha256:fe10ec77c93ddf3d13a73b035abaac7a9f5e436513864ccdad516693213c65d6", size = 164437, upload-time = "2025-10-27T17:57:37.538Z" }, -] - -[package.optional-dependencies] -css = [ - { name = "tinycss2" }, -] - -[[package]] -name = "blinker" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" }, -] - -[[package]] -name = "cairocffi" -version = "1.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/c5/1a4dc131459e68a173cbdab5fad6b524f53f9c1ef7861b7698e998b837cc/cairocffi-1.7.1.tar.gz", hash = "sha256:2e48ee864884ec4a3a34bfa8c9ab9999f688286eb714a15a43ec9d068c36557b", size = 88096, upload-time = "2024-06-18T10:56:06.741Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/93/d8/ba13451aa6b745c49536e87b6bf8f629b950e84bd0e8308f7dc6883b67e2/cairocffi-1.7.1-py3-none-any.whl", hash = "sha256:9803a0e11f6c962f3b0ae2ec8ba6ae45e957a146a004697a1ac1bbf16b073b3f", size = 75611, upload-time = "2024-06-18T10:55:59.489Z" }, -] - -[[package]] -name = "camel-ai" -version = "0.2.78" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "astor" }, - { name = "colorama" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jsonschema" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pillow" }, - { name = "psutil" }, - { name = "pydantic" }, - { name = "tiktoken" }, - { name = "websockets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/2b/cd5181bfd0ebcf567a088ee5c1e3768b132ba4b1489ee19d5fb0bd679586/camel_ai-0.2.78.tar.gz", hash = "sha256:24745da225da7da96dcd85f72d143c6104569c17f14280c369d7e82b86851284", size = 964632, upload-time = "2025-10-15T17:20:54.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/81/0cfb1c0d9da589665e2eb4471887967e70bba428638c37fb4f6a78baf300/camel_ai-0.2.78-py3-none-any.whl", hash = "sha256:356624da13dfe0c55ef43dc509c18ce029f67fe3997966495a4ce9be931078d5", size = 1415578, upload-time = "2025-10-15T17:20:51.727Z" }, -] - -[[package]] -name = "camel-oasis" -version = "0.2.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cairocffi" }, - { name = "camel-ai" }, - { name = "igraph" }, - { name = "neo4j" }, - { name = "openapi-spec-validator" }, - { name = "pandas" }, - { name = "pillow" }, - { name = "prance" }, - { name = "pre-commit" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "requests-oauthlib" }, - { name = "sentence-transformers" }, - { name = "slack-sdk" }, - { name = "unstructured" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d9/6f/b36240380c65397f3e18829fce8ed0a1d19893b32d3596aa0902c7b3ad81/camel_oasis-0.2.5.tar.gz", hash = "sha256:f667dec86f9f7823d50f76b07733a34afc1427b923f1a673519206bb41a57f8c", size = 56966, upload-time = "2025-12-04T11:58:19.43Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d0/6d62173602433937d0228b7809e2b244e09c11cfb1be9c6754ae3b20d887/camel_oasis-0.2.5-py3-none-any.whl", hash = "sha256:9ebd6ba8e331495ee56b25cc63982188b94125dde499e5e9c00398a1d47e606d", size = 75954, upload-time = "2025-12-04T11:58:18.363Z" }, -] - -[[package]] -name = "certifi" -version = "2025.11.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538, upload-time = "2025-11-12T02:54:51.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438, upload-time = "2025-11-12T02:54:49.735Z" }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, -] - -[[package]] -name = "cfgv" -version = "3.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, -] - -[[package]] -name = "chardet" -version = "5.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/f7b6ab21ec75897ed80c17d79b15951a719226b9fababf1e40ea74d69079/chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7", size = 2069618, upload-time = "2023-08-01T19:23:02.662Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/6f/f5fbc992a329ee4e0f288c1fe0e2ad9485ed064cac731ed2fe47dcc38cbf/chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970", size = 199385, upload-time = "2023-08-01T19:23:00.661Z" }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" }, - { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" }, - { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" }, - { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" }, - { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" }, - { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" }, - { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" }, - { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" }, - { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" }, - { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" }, - { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" }, - { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" }, - { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "cryptography" -version = "46.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, - { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" }, - { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" }, - { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" }, - { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" }, -] - -[[package]] -name = "dataclasses-json" -version = "0.6.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "marshmallow" }, - { name = "typing-inspect" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, -] - -[[package]] -name = "decorator" -version = "5.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, -] - -[[package]] -name = "defusedxml" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, -] - -[[package]] -name = "distlib" -version = "0.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - -[[package]] -name = "docopt" -version = "0.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } - -[[package]] -name = "docstring-parser" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, -] - -[[package]] -name = "emoji" -version = "2.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/78/0d2db9382c92a163d7095fc08efff7800880f830a152cfced40161e7638d/emoji-2.15.0.tar.gz", hash = "sha256:eae4ab7d86456a70a00a985125a03263a5eac54cd55e51d7e184b1ed3b6757e4", size = 615483, upload-time = "2025-09-21T12:13:02.755Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/5e/4b5aaaabddfacfe36ba7768817bd1f71a7a810a43705e531f3ae4c690767/emoji-2.15.0-py3-none-any.whl", hash = "sha256:205296793d66a89d88af4688fa57fd6496732eb48917a87175a023c8138995eb", size = 608433, upload-time = "2025-09-21T12:13:01.197Z" }, -] - -[[package]] -name = "executing" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, -] - -[[package]] -name = "fastjsonschema" -version = "2.21.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/b5/23b216d9d985a956623b6bd12d4086b60f0059b27799f23016af04a74ea1/fastjsonschema-2.21.2.tar.gz", hash = "sha256:b1eb43748041c880796cd077f1a07c3d94e93ae84bba5ed36800a33554ae05de", size = 374130, upload-time = "2025-08-14T18:49:36.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/a8/20d0723294217e47de6d9e2e40fd4a9d2f7c4b6ef974babd482a59743694/fastjsonschema-2.21.2-py3-none-any.whl", hash = "sha256:1c797122d0a86c5cace2e54bf4e819c36223b552017172f32c5c024a6b77e463", size = 24024, upload-time = "2025-08-14T18:49:34.776Z" }, -] - -[[package]] -name = "filelock" -version = "3.20.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/23/ce7a1126827cedeb958fc043d61745754464eb56c5937c35bbf2b8e26f34/filelock-3.20.1.tar.gz", hash = "sha256:b8360948b351b80f420878d8516519a2204b07aefcdcfd24912a5d33127f188c", size = 19476, upload-time = "2025-12-15T23:54:28.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/7f/a1a97644e39e7316d850784c642093c99df1290a460df4ede27659056834/filelock-3.20.1-py3-none-any.whl", hash = "sha256:15d9e9a67306188a44baa72f569d2bfd803076269365fdea0934385da4dc361a", size = 16666, upload-time = "2025-12-15T23:54:26.874Z" }, -] - -[[package]] -name = "filetype" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/29/745f7d30d47fe0f251d3ad3dc2978a23141917661998763bebb6da007eb1/filetype-1.2.0.tar.gz", hash = "sha256:66b56cd6474bf41d8c54660347d37afcc3f7d1970648de365c102ef77548aadb", size = 998020, upload-time = "2022-11-02T17:34:04.141Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, -] - -[[package]] -name = "flask" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "blinker" }, - { name = "click" }, - { name = "itsdangerous" }, - { name = "jinja2" }, - { name = "markupsafe" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" }, -] - -[[package]] -name = "flask-cors" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/70/74/0fc0fa68d62f21daef41017dafab19ef4b36551521260987eb3a5394c7ba/flask_cors-6.0.2.tar.gz", hash = "sha256:6e118f3698249ae33e429760db98ce032a8bf9913638d085ca0f4c5534ad2423", size = 13472, upload-time = "2025-12-12T20:31:42.861Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, -] - -[[package]] -name = "fsspec" -version = "2025.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, -] - -[[package]] -name = "hf-xet" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, - { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, - { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, - { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, - { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, - { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, - { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - -[[package]] -name = "huggingface-hub" -version = "0.36.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, -] - -[[package]] -name = "identify" -version = "2.6.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, -] - -[[package]] -name = "igraph" -version = "0.11.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "texttable" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a0/1f70c34a96dcb0acf428319e83655e92ab2955d73a33f711852a5fb79681/igraph-0.11.6.tar.gz", hash = "sha256:837f233256c3319f2a35a6a80d94eafe47b43791ef4c6f9e9871061341ac8e28", size = 4559252, upload-time = "2024-07-08T23:38:32.722Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/f5/2de2ff541a014c3387c0c570a91e51b80643d9a2a3e0dec8030bcec3083d/igraph-0.11.6-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:8aabef03d787b519d1075dfc0da4a1109fb113b941334883e3e7947ac30a459e", size = 1945799, upload-time = "2024-07-08T23:37:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3b/5cf3b131d433dea61608ea2c27bebf74de9cc7a50ced0b26311d6288294e/igraph-0.11.6-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1f2cc4a518d99cdf6cae514f85e93e56852bc8c325b3abb96037d1d690b5975f", size = 1751328, upload-time = "2024-07-08T23:37:34.415Z" }, - { url = "https://files.pythonhosted.org/packages/fc/71/71cd93c1b26e6051ef5dfad94333690188e663942e54bea6bbfff79d2dbe/igraph-0.11.6-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e859238be52ab8ccc614d18f9362942bc88ce543afc12548f81ae99b10801d", size = 2974608, upload-time = "2024-07-08T23:37:36.076Z" }, - { url = "https://files.pythonhosted.org/packages/19/ec/02e596595776367f5fd07c65a56ff1593680273f935c481975fe6a55e67f/igraph-0.11.6-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d61fbe5e85eb4ae9efe08c461f9bdeedb02a2b5739fbc223d324a71f40a28be2", size = 3054685, upload-time = "2024-07-08T23:37:38.121Z" }, - { url = "https://files.pythonhosted.org/packages/02/f2/70849b7ff0fbfd3e7c964ac737ea973d0121d30a32a18894df6d0843bd0c/igraph-0.11.6-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6620ba39df29fd42151becf82309b54e57148233c9c3ef890eed62e25eed8a5", size = 3134936, upload-time = "2024-07-08T23:37:40.497Z" }, - { url = "https://files.pythonhosted.org/packages/ef/49/579f3a5daea1a672f05c95ead488e779f1479a8efb78715c1508f7dae58a/igraph-0.11.6-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:59666589bb3d07f310cda2c5106a8adeeb77c2ef27fecf1c6438b6091f4ca69d", size = 3887705, upload-time = "2024-07-08T23:37:42.914Z" }, - { url = "https://files.pythonhosted.org/packages/ef/d2/66203ba13f90ea77f541c0f5b5ae095a7ced0a564486f576e5adce0d0bd7/igraph-0.11.6-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:8750b6d6caebf199cf7dc41c931f58e330153779707391e30f0a29f02666fb6e", size = 4158876, upload-time = "2024-07-08T23:37:45.708Z" }, - { url = "https://files.pythonhosted.org/packages/53/e7/3fbe83625efb1dd5f387a9da9ec0ac779a2dc42e6db0ac7eb54c44943b2f/igraph-0.11.6-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:967d6f2c30fe94317da15e459374d0fb8ca3e56020412f201ecd07dd5b5352f2", size = 4050715, upload-time = "2024-07-08T23:37:48.478Z" }, - { url = "https://files.pythonhosted.org/packages/ab/91/2535aa81a1de36f5b21df129b3434a18710d31ea56efbf0ec6e1f1958fda/igraph-0.11.6-cp39-abi3-win32.whl", hash = "sha256:9744f95a67319eb6cb487ceabf30f5d7940de34bada51f0ba63adbd23e0f94ad", size = 1589990, upload-time = "2024-07-08T23:37:50.825Z" }, - { url = "https://files.pythonhosted.org/packages/08/4a/e781867fa2fb41d823a8f1978ac464aef3d78bb73c6f40589a74cc47bf42/igraph-0.11.6-cp39-abi3-win_amd64.whl", hash = "sha256:b80e69eb11faa9c57330a9ffebdde5808966efe1c1f638d4d4827ea04df7aca8", size = 1965492, upload-time = "2024-07-08T23:37:53.203Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "ipython" -version = "8.12.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "appnope", marker = "sys_platform == 'darwin'" }, - { name = "backcall" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "decorator" }, - { name = "jedi" }, - { name = "matplotlib-inline" }, - { name = "pexpect", marker = "sys_platform != 'win32'" }, - { name = "pickleshare" }, - { name = "prompt-toolkit" }, - { name = "pygments" }, - { name = "stack-data" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9e/6a/44ef299b1762f5a73841e87fae8a73a8cc8aee538d6dc8c77a5afe1fd2ce/ipython-8.12.3.tar.gz", hash = "sha256:3910c4b54543c2ad73d06579aa771041b7d5707b033bd488669b4cf544e3b363", size = 5470171, upload-time = "2023-09-29T09:14:37.468Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/97/8fe103906cd81bc42d3b0175b5534a9f67dccae47d6451131cf8d0d70bb2/ipython-8.12.3-py3-none-any.whl", hash = "sha256:b0340d46a933d27c657b211a329d0be23793c36595acf9e6ef4164bc01a1804c", size = 798307, upload-time = "2023-09-29T09:14:34.431Z" }, -] - -[[package]] -name = "itsdangerous" -version = "2.2.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" }, -] - -[[package]] -name = "jedi" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "parso" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, -] - -[[package]] -name = "jiter" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294, upload-time = "2025-11-09T20:49:23.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/f9/eaca4633486b527ebe7e681c431f529b63fe2709e7c5242fc0f43f77ce63/jiter-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8f8a7e317190b2c2d60eb2e8aa835270b008139562d70fe732e1c0020ec53c9", size = 316435, upload-time = "2025-11-09T20:47:02.087Z" }, - { url = "https://files.pythonhosted.org/packages/10/c1/40c9f7c22f5e6ff715f28113ebaba27ab85f9af2660ad6e1dd6425d14c19/jiter-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2218228a077e784c6c8f1a8e5d6b8cb1dea62ce25811c356364848554b2056cd", size = 320548, upload-time = "2025-11-09T20:47:03.409Z" }, - { url = "https://files.pythonhosted.org/packages/6b/1b/efbb68fe87e7711b00d2cfd1f26bb4bfc25a10539aefeaa7727329ffb9cb/jiter-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9354ccaa2982bf2188fd5f57f79f800ef622ec67beb8329903abf6b10da7d423", size = 351915, upload-time = "2025-11-09T20:47:05.171Z" }, - { url = "https://files.pythonhosted.org/packages/15/2d/c06e659888c128ad1e838123d0638f0efad90cc30860cb5f74dd3f2fc0b3/jiter-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2607185ea89b4af9a604d4c7ec40e45d3ad03ee66998b031134bc510232bb7", size = 368966, upload-time = "2025-11-09T20:47:06.508Z" }, - { url = "https://files.pythonhosted.org/packages/6b/20/058db4ae5fb07cf6a4ab2e9b9294416f606d8e467fb74c2184b2a1eeacba/jiter-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a585a5e42d25f2e71db5f10b171f5e5ea641d3aa44f7df745aa965606111cc2", size = 482047, upload-time = "2025-11-09T20:47:08.382Z" }, - { url = "https://files.pythonhosted.org/packages/49/bb/dc2b1c122275e1de2eb12905015d61e8316b2f888bdaac34221c301495d6/jiter-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd9e21d34edff5a663c631f850edcb786719c960ce887a5661e9c828a53a95d9", size = 380835, upload-time = "2025-11-09T20:47:09.81Z" }, - { url = "https://files.pythonhosted.org/packages/23/7d/38f9cd337575349de16da575ee57ddb2d5a64d425c9367f5ef9e4612e32e/jiter-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a612534770470686cd5431478dc5a1b660eceb410abade6b1b74e320ca98de6", size = 364587, upload-time = "2025-11-09T20:47:11.529Z" }, - { url = "https://files.pythonhosted.org/packages/f0/a3/b13e8e61e70f0bb06085099c4e2462647f53cc2ca97614f7fedcaa2bb9f3/jiter-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3985aea37d40a908f887b34d05111e0aae822943796ebf8338877fee2ab67725", size = 390492, upload-time = "2025-11-09T20:47:12.993Z" }, - { url = "https://files.pythonhosted.org/packages/07/71/e0d11422ed027e21422f7bc1883c61deba2d9752b720538430c1deadfbca/jiter-0.12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b1207af186495f48f72529f8d86671903c8c10127cac6381b11dddc4aaa52df6", size = 522046, upload-time = "2025-11-09T20:47:14.6Z" }, - { url = "https://files.pythonhosted.org/packages/9f/59/b968a9aa7102a8375dbbdfbd2aeebe563c7e5dddf0f47c9ef1588a97e224/jiter-0.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef2fb241de583934c9915a33120ecc06d94aa3381a134570f59eed784e87001e", size = 513392, upload-time = "2025-11-09T20:47:16.011Z" }, - { url = "https://files.pythonhosted.org/packages/ca/e4/7df62002499080dbd61b505c5cb351aa09e9959d176cac2aa8da6f93b13b/jiter-0.12.0-cp311-cp311-win32.whl", hash = "sha256:453b6035672fecce8007465896a25b28a6b59cfe8fbc974b2563a92f5a92a67c", size = 206096, upload-time = "2025-11-09T20:47:17.344Z" }, - { url = "https://files.pythonhosted.org/packages/bb/60/1032b30ae0572196b0de0e87dce3b6c26a1eff71aad5fe43dee3082d32e0/jiter-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:ca264b9603973c2ad9435c71a8ec8b49f8f715ab5ba421c85a51cde9887e421f", size = 204899, upload-time = "2025-11-09T20:47:19.365Z" }, - { url = "https://files.pythonhosted.org/packages/49/d5/c145e526fccdb834063fb45c071df78b0cc426bbaf6de38b0781f45d956f/jiter-0.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:cb00ef392e7d684f2754598c02c409f376ddcef857aae796d559e6cacc2d78a5", size = 188070, upload-time = "2025-11-09T20:47:20.75Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449, upload-time = "2025-11-09T20:47:22.999Z" }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855, upload-time = "2025-11-09T20:47:24.779Z" }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171, upload-time = "2025-11-09T20:47:26.469Z" }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590, upload-time = "2025-11-09T20:47:27.918Z" }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462, upload-time = "2025-11-09T20:47:29.654Z" }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983, upload-time = "2025-11-09T20:47:31.026Z" }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328, upload-time = "2025-11-09T20:47:33.286Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740, upload-time = "2025-11-09T20:47:34.703Z" }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875, upload-time = "2025-11-09T20:47:36.058Z" }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457, upload-time = "2025-11-09T20:47:37.932Z" }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546, upload-time = "2025-11-09T20:47:40.47Z" }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196, upload-time = "2025-11-09T20:47:41.794Z" }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100, upload-time = "2025-11-09T20:47:43.007Z" }, - { url = "https://files.pythonhosted.org/packages/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" }, - { url = "https://files.pythonhosted.org/packages/21/01/857d4608f5edb0664aa791a3d45702e1a5bcfff9934da74035e7b9803846/jiter-0.12.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd2097de91cf03eaa27b3cbdb969addf83f0179c6afc41bbc4513705e013c65d", size = 347212, upload-time = "2025-11-09T20:49:15.643Z" }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974, upload-time = "2025-11-09T20:49:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233, upload-time = "2025-11-09T20:49:18.734Z" }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537, upload-time = "2025-11-09T20:49:20.317Z" }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110, upload-time = "2025-11-09T20:49:21.817Z" }, -] - -[[package]] -name = "joblib" -version = "1.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, -] - -[[package]] -name = "jsonschema" -version = "4.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" }, -] - -[[package]] -name = "jsonschema-path" -version = "0.3.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pathable" }, - { name = "pyyaml" }, - { name = "referencing" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, -] - -[[package]] -name = "jupyter-client" -version = "8.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-core" }, - { name = "python-dateutil" }, - { name = "pyzmq" }, - { name = "tornado" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a6/27/d10de45e8ad4ce872372c4a3a37b7b35b6b064f6f023a5c14ffcced4d59d/jupyter_client-8.7.0.tar.gz", hash = "sha256:3357212d9cbe01209e59190f67a3a7e1f387a4f4e88d1e0433ad84d7b262531d", size = 344691, upload-time = "2025-12-09T18:37:01.953Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl", hash = "sha256:3671a94fd25e62f5f2f554f5e95389c2294d89822378a5f2dd24353e1494a9e0", size = 106215, upload-time = "2025-12-09T18:37:00.024Z" }, -] - -[[package]] -name = "jupyter-core" -version = "5.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "platformdirs" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, -] - -[[package]] -name = "jupyterlab-pygments" -version = "0.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, -] - -[[package]] -name = "langdetect" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" } - -[[package]] -name = "lazy-object-proxy" -version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/01/b3/4684b1e128a87821e485f5a901b179790e6b5bc02f89b7ee19c23be36ef3/lazy_object_proxy-1.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1cf69cd1a6c7fe2dbcc3edaa017cf010f4192e53796538cc7d5e1fedbfa4bcff", size = 26656, upload-time = "2025-08-22T13:42:30.605Z" }, - { url = "https://files.pythonhosted.org/packages/3a/03/1bdc21d9a6df9ff72d70b2ff17d8609321bea4b0d3cffd2cea92fb2ef738/lazy_object_proxy-1.12.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efff4375a8c52f55a145dc8487a2108c2140f0bec4151ab4e1843e52eb9987ad", size = 68832, upload-time = "2025-08-22T13:42:31.675Z" }, - { url = "https://files.pythonhosted.org/packages/3d/4b/5788e5e8bd01d19af71e50077ab020bc5cce67e935066cd65e1215a09ff9/lazy_object_proxy-1.12.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1192e8c2f1031a6ff453ee40213afa01ba765b3dc861302cd91dbdb2e2660b00", size = 69148, upload-time = "2025-08-22T13:42:32.876Z" }, - { url = "https://files.pythonhosted.org/packages/79/0e/090bf070f7a0de44c61659cb7f74c2fe02309a77ca8c4b43adfe0b695f66/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3605b632e82a1cbc32a1e5034278a64db555b3496e0795723ee697006b980508", size = 67800, upload-time = "2025-08-22T13:42:34.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d2/b320325adbb2d119156f7c506a5fbfa37fcab15c26d13cf789a90a6de04e/lazy_object_proxy-1.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a61095f5d9d1a743e1e20ec6d6db6c2ca511961777257ebd9b288951b23b44fa", size = 68085, upload-time = "2025-08-22T13:42:35.197Z" }, - { url = "https://files.pythonhosted.org/packages/6a/48/4b718c937004bf71cd82af3713874656bcb8d0cc78600bf33bb9619adc6c/lazy_object_proxy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:997b1d6e10ecc6fb6fe0f2c959791ae59599f41da61d652f6c903d1ee58b7370", size = 26535, upload-time = "2025-08-22T13:42:36.521Z" }, - { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, - { url = "https://files.pythonhosted.org/packages/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" }, -] - -[[package]] -name = "lxml" -version = "6.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62", size = 4073426, upload-time = "2025-09-22T04:04:59.287Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/d5/becbe1e2569b474a23f0c672ead8a29ac50b2dc1d5b9de184831bda8d14c/lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607", size = 8634365, upload-time = "2025-09-22T04:00:45.672Z" }, - { url = "https://files.pythonhosted.org/packages/28/66/1ced58f12e804644426b85d0bb8a4478ca77bc1761455da310505f1a3526/lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938", size = 4650793, upload-time = "2025-09-22T04:00:47.783Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/549098ffea39dfd167e3f174b4ce983d0eed61f9d8d25b7bf2a57c3247fc/lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d", size = 4944362, upload-time = "2025-09-22T04:00:49.845Z" }, - { url = "https://files.pythonhosted.org/packages/ac/bd/f207f16abf9749d2037453d56b643a7471d8fde855a231a12d1e095c4f01/lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438", size = 5083152, upload-time = "2025-09-22T04:00:51.709Z" }, - { url = "https://files.pythonhosted.org/packages/15/ae/bd813e87d8941d52ad5b65071b1affb48da01c4ed3c9c99e40abb266fbff/lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964", size = 5023539, upload-time = "2025-09-22T04:00:53.593Z" }, - { url = "https://files.pythonhosted.org/packages/02/cd/9bfef16bd1d874fbe0cb51afb00329540f30a3283beb9f0780adbb7eec03/lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d", size = 5344853, upload-time = "2025-09-22T04:00:55.524Z" }, - { url = "https://files.pythonhosted.org/packages/b8/89/ea8f91594bc5dbb879734d35a6f2b0ad50605d7fb419de2b63d4211765cc/lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7", size = 5225133, upload-time = "2025-09-22T04:00:57.269Z" }, - { url = "https://files.pythonhosted.org/packages/b9/37/9c735274f5dbec726b2db99b98a43950395ba3d4a1043083dba2ad814170/lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178", size = 4677944, upload-time = "2025-09-22T04:00:59.052Z" }, - { url = "https://files.pythonhosted.org/packages/20/28/7dfe1ba3475d8bfca3878365075abe002e05d40dfaaeb7ec01b4c587d533/lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553", size = 5284535, upload-time = "2025-09-22T04:01:01.335Z" }, - { url = "https://files.pythonhosted.org/packages/e7/cf/5f14bc0de763498fc29510e3532bf2b4b3a1c1d5d0dff2e900c16ba021ef/lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb", size = 5067343, upload-time = "2025-09-22T04:01:03.13Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b0/bb8275ab5472f32b28cfbbcc6db7c9d092482d3439ca279d8d6fa02f7025/lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a", size = 4725419, upload-time = "2025-09-22T04:01:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/25/4c/7c222753bc72edca3b99dbadba1b064209bc8ed4ad448af990e60dcce462/lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c", size = 5275008, upload-time = "2025-09-22T04:01:07.327Z" }, - { url = "https://files.pythonhosted.org/packages/6c/8c/478a0dc6b6ed661451379447cdbec77c05741a75736d97e5b2b729687828/lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7", size = 5248906, upload-time = "2025-09-22T04:01:09.452Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d9/5be3a6ab2784cdf9accb0703b65e1b64fcdd9311c9f007630c7db0cfcce1/lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46", size = 3610357, upload-time = "2025-09-22T04:01:11.102Z" }, - { url = "https://files.pythonhosted.org/packages/e2/7d/ca6fb13349b473d5732fb0ee3eec8f6c80fc0688e76b7d79c1008481bf1f/lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078", size = 4036583, upload-time = "2025-09-22T04:01:12.766Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a2/51363b5ecd3eab46563645f3a2c3836a2fc67d01a1b87c5017040f39f567/lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285", size = 3680591, upload-time = "2025-09-22T04:01:14.874Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c8/8ff2bc6b920c84355146cd1ab7d181bc543b89241cfb1ebee824a7c81457/lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456", size = 8661887, upload-time = "2025-09-22T04:01:17.265Z" }, - { url = "https://files.pythonhosted.org/packages/37/6f/9aae1008083bb501ef63284220ce81638332f9ccbfa53765b2b7502203cf/lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924", size = 4667818, upload-time = "2025-09-22T04:01:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ca/31fb37f99f37f1536c133476674c10b577e409c0a624384147653e38baf2/lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f", size = 4950807, upload-time = "2025-09-22T04:01:21.487Z" }, - { url = "https://files.pythonhosted.org/packages/da/87/f6cb9442e4bada8aab5ae7e1046264f62fdbeaa6e3f6211b93f4c0dd97f1/lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534", size = 5109179, upload-time = "2025-09-22T04:01:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/c8/20/a7760713e65888db79bbae4f6146a6ae5c04e4a204a3c48896c408cd6ed2/lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564", size = 5023044, upload-time = "2025-09-22T04:01:25.118Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b0/7e64e0460fcb36471899f75831509098f3fd7cd02a3833ac517433cb4f8f/lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f", size = 5359685, upload-time = "2025-09-22T04:01:27.398Z" }, - { url = "https://files.pythonhosted.org/packages/b9/e1/e5df362e9ca4e2f48ed6411bd4b3a0ae737cc842e96877f5bf9428055ab4/lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0", size = 5654127, upload-time = "2025-09-22T04:01:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d1/232b3309a02d60f11e71857778bfcd4acbdb86c07db8260caf7d008b08f8/lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192", size = 5253958, upload-time = "2025-09-22T04:01:31.535Z" }, - { url = "https://files.pythonhosted.org/packages/35/35/d955a070994725c4f7d80583a96cab9c107c57a125b20bb5f708fe941011/lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0", size = 4711541, upload-time = "2025-09-22T04:01:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/1e/be/667d17363b38a78c4bd63cfd4b4632029fd68d2c2dc81f25ce9eb5224dd5/lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092", size = 5267426, upload-time = "2025-09-22T04:01:35.639Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/62c70aa4a1c26569bc958c9ca86af2bb4e1f614e8c04fb2989833874f7ae/lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f", size = 5064917, upload-time = "2025-09-22T04:01:37.448Z" }, - { url = "https://files.pythonhosted.org/packages/bd/55/6ceddaca353ebd0f1908ef712c597f8570cc9c58130dbb89903198e441fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8", size = 4788795, upload-time = "2025-09-22T04:01:39.165Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e8/fd63e15da5e3fd4c2146f8bbb3c14e94ab850589beab88e547b2dbce22e1/lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f", size = 5676759, upload-time = "2025-09-22T04:01:41.506Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/b3ec58dc5c374697f5ba37412cd2728f427d056315d124dd4b61da381877/lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6", size = 5255666, upload-time = "2025-09-22T04:01:43.363Z" }, - { url = "https://files.pythonhosted.org/packages/19/93/03ba725df4c3d72afd9596eef4a37a837ce8e4806010569bedfcd2cb68fd/lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322", size = 5277989, upload-time = "2025-09-22T04:01:45.215Z" }, - { 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/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" }, - { url = "https://files.pythonhosted.org/packages/7a/c1/27428a2ff348e994ab4f8777d3a0ad510b6b92d37718e5887d2da99952a2/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9", size = 4272119, upload-time = "2025-09-22T04:04:51.801Z" }, - { url = "https://files.pythonhosted.org/packages/f0/d0/3020fa12bcec4ab62f97aab026d57c2f0cfd480a558758d9ca233bb6a79d/lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a", size = 4417314, upload-time = "2025-09-22T04:04:55.024Z" }, - { url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, - { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, - { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, - { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, - { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, - { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, - { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, - { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, - { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, - { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, - { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, - { 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" }, -] - -[[package]] -name = "marshmallow" -version = "3.26.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ab/5e/5e53d26b42ab75491cda89b871dab9e97c840bf12c63ec58a1919710cd06/marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6", size = 221825, upload-time = "2025-02-03T15:32:25.093Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/75/51952c7b2d3873b44a0028b1bd26a25078c18f92f256608e8d1dc61b39fd/marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c", size = 50878, upload-time = "2025-02-03T15:32:22.295Z" }, -] - -[[package]] -name = "matplotlib-inline" -version = "0.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" }, -] - -[[package]] -name = "mcp" -version = "1.24.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d6/2c/db9ae5ab1fcdd9cd2bcc7ca3b7361b712e30590b64d5151a31563af8f82d/mcp-1.24.0.tar.gz", hash = "sha256:aeaad134664ce56f2721d1abf300666a1e8348563f4d3baff361c3b652448efc", size = 604375, upload-time = "2025-12-12T14:19:38.205Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/0d/5cf14e177c8ae655a2fd9324a6ef657ca4cafd3fc2201c87716055e29641/mcp-1.24.0-py3-none-any.whl", hash = "sha256:db130e103cc50ddc3dffc928382f33ba3eaef0b711f7a87c05e7ded65b1ca062", size = 232896, upload-time = "2025-12-12T14:19:36.14Z" }, -] - -[[package]] -name = "mirofish-backend" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "camel-ai" }, - { name = "camel-oasis" }, - { name = "chardet" }, - { name = "charset-normalizer" }, - { name = "flask" }, - { name = "flask-cors" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "pymupdf" }, - { name = "python-dotenv" }, - { name = "zep-cloud" }, -] - -[package.optional-dependencies] -dev = [ - { name = "pipreqs" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, -] - -[package.dev-dependencies] -dev = [ - { name = "pytest" }, - { name = "pytest-asyncio" }, -] - -[package.metadata] -requires-dist = [ - { name = "camel-ai", specifier = "==0.2.78" }, - { name = "camel-oasis", specifier = "==0.2.5" }, - { name = "chardet", specifier = ">=5.0.0" }, - { name = "charset-normalizer", specifier = ">=3.0.0" }, - { name = "flask", specifier = ">=3.0.0" }, - { name = "flask-cors", specifier = ">=6.0.0" }, - { name = "openai", specifier = ">=1.0.0" }, - { name = "pipreqs", marker = "extra == 'dev'", specifier = ">=0.5.0" }, - { name = "pydantic", specifier = ">=2.0.0" }, - { name = "pymupdf", specifier = ">=1.24.0" }, - { 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.13.0" }, -] -provides-extras = ["dev"] - -[package.metadata.requires-dev] -dev = [ - { name = "pytest", specifier = ">=8.0.0" }, - { name = "pytest-asyncio", specifier = ">=0.23.0" }, -] - -[[package]] -name = "mistune" -version = "3.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/02/a7fb8b21d4d55ac93cdcde9d3638da5dd0ebdd3a4fed76c7725e10b81cbe/mistune-3.1.4.tar.gz", hash = "sha256:b5a7f801d389f724ec702840c11d8fc48f2b33519102fc7ee739e8177b672164", size = 94588, upload-time = "2025-08-29T07:20:43.594Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/f0/8282d9641415e9e33df173516226b404d367a0fc55e1a60424a152913abc/mistune-3.1.4-py3-none-any.whl", hash = "sha256:93691da911e5d9d2e23bc54472892aff676df27a75274962ff9edc210364266d", size = 53481, upload-time = "2025-08-29T07:20:42.218Z" }, -] - -[[package]] -name = "mpmath" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - -[[package]] -name = "nbclient" -version = "0.10.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jupyter-client" }, - { name = "jupyter-core" }, - { name = "nbformat" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, -] - -[[package]] -name = "nbconvert" -version = "7.16.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "beautifulsoup4" }, - { name = "bleach", extra = ["css"] }, - { name = "defusedxml" }, - { name = "jinja2" }, - { name = "jupyter-core" }, - { name = "jupyterlab-pygments" }, - { name = "markupsafe" }, - { name = "mistune" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "packaging" }, - { name = "pandocfilters" }, - { name = "pygments" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" }, -] - -[[package]] -name = "nbformat" -version = "5.10.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastjsonschema" }, - { name = "jsonschema" }, - { name = "jupyter-core" }, - { name = "traitlets" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, -] - -[[package]] -name = "neo4j" -version = "5.23.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytz" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e1/14/86f149430f2e6ffe63f5726951720b7cf4136ee1676eb077c4ade94959df/neo4j-5.23.0.tar.gz", hash = "sha256:26b06dac3a4b93d882a61714c5ca8d06fe68f697cbdfe113ab840d651a2d46a2", size = 215255, upload-time = "2024-07-29T10:22:29.973Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/10/0feaa10a342f5c06ffc5bfe3e5e52a9950094186889208251de39092237b/neo4j-5.23.0-py3-none-any.whl", hash = "sha256:5d8d2f45227c12d6ba564720cbc3e2f57aac472e4fa14fe69270e4f952791020", size = 293671, upload-time = "2024-07-29T10:22:24.874Z" }, -] - -[[package]] -name = "networkx" -version = "3.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, -] - -[[package]] -name = "nltk" -version = "3.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "joblib" }, - { name = "regex" }, - { name = "tqdm" }, -] -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/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]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, -] - -[[package]] -name = "numpy" -version = "2.3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" }, - { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" }, - { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" }, - { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" }, - { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" }, - { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" }, - { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" }, - { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" }, - { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" }, - { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" }, - { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" }, - { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" }, - { 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/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" }, - { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" }, - { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" }, -] - -[[package]] -name = "nvidia-cublas-cu12" -version = "12.8.4.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, -] - -[[package]] -name = "nvidia-cuda-cupti-cu12" -version = "12.8.90" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, -] - -[[package]] -name = "nvidia-cuda-nvrtc-cu12" -version = "12.8.93" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, -] - -[[package]] -name = "nvidia-cuda-runtime-cu12" -version = "12.8.90" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, -] - -[[package]] -name = "nvidia-cudnn-cu12" -version = "9.10.2.21" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, -] - -[[package]] -name = "nvidia-cufft-cu12" -version = "11.3.3.83" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, -] - -[[package]] -name = "nvidia-cufile-cu12" -version = "1.13.1.3" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, -] - -[[package]] -name = "nvidia-curand-cu12" -version = "10.3.9.90" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, -] - -[[package]] -name = "nvidia-cusolver-cu12" -version = "11.7.3.90" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-cublas-cu12" }, - { name = "nvidia-cusparse-cu12" }, - { name = "nvidia-nvjitlink-cu12" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, -] - -[[package]] -name = "nvidia-cusparse-cu12" -version = "12.5.8.93" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nvidia-nvjitlink-cu12" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, -] - -[[package]] -name = "nvidia-cusparselt-cu12" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, -] - -[[package]] -name = "nvidia-nccl-cu12" -version = "2.27.5" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, -] - -[[package]] -name = "nvidia-nvjitlink-cu12" -version = "12.8.93" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, -] - -[[package]] -name = "nvidia-nvshmem-cu12" -version = "3.3.20" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" }, -] - -[[package]] -name = "nvidia-nvtx-cu12" -version = "12.8.90" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, -] - -[[package]] -name = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, -] - -[[package]] -name = "openai" -version = "1.109.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c6/a1/a303104dc55fc546a3f6914c842d3da471c64eec92043aef8f652eb6c524/openai-1.109.1.tar.gz", hash = "sha256:d173ed8dbca665892a6db099b4a2dfac624f94d20a93f46eb0b56aae940ed869", size = 564133, upload-time = "2025-09-24T13:00:53.075Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" }, -] - -[[package]] -name = "openapi-schema-validator" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-specifications" }, - { name = "rfc3339-validator" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, -] - -[[package]] -name = "openapi-spec-validator" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "lazy-object-proxy" }, - { name = "openapi-schema-validator" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/fe/21954ff978239dc29ebb313f5c87eeb4ec929b694b9667323086730998e2/openapi_spec_validator-0.7.1.tar.gz", hash = "sha256:8577b85a8268685da6f8aa30990b83b7960d4d1117e901d451b5d572605e5ec7", size = 37985, upload-time = "2023-10-13T11:43:40.53Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/4d/e744fff95aaf3aeafc968d5ba7297c8cda0d1ecb8e3acd21b25adae4d835/openapi_spec_validator-0.7.1-py3-none-any.whl", hash = "sha256:3c81825043f24ccbcd2f4b149b11e8231abce5ba84f37065e14ec947d8f4e959", size = 38998, upload-time = "2023-10-13T11:43:38.371Z" }, -] - -[[package]] -name = "packaging" -version = "25.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, -] - -[[package]] -name = "pandas" -version = "2.2.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/d9/ecf715f34c73ccb1d8ceb82fc01cd1028a65a5f6dbc57bfa6ea155119058/pandas-2.2.2.tar.gz", hash = "sha256:9e79019aba43cb4fda9e4d983f8e88ca0373adbb697ae9c6c43093218de28b54", size = 4398391, upload-time = "2024-04-10T19:45:48.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/70/61704497903d43043e288017cb2b82155c0d41e15f5c17807920877b45c2/pandas-2.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:696039430f7a562b74fa45f540aca068ea85fa34c244d0deee539cb6d70aa288", size = 12574808, upload-time = "2024-04-10T19:44:35.516Z" }, - { url = "https://files.pythonhosted.org/packages/16/c6/75231fd47afd6b3f89011e7077f1a3958441264aca7ae9ff596e3276a5d0/pandas-2.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e90497254aacacbc4ea6ae5e7a8cd75629d6ad2b30025a4a8b09aa4faf55151", size = 11304876, upload-time = "2024-04-10T19:44:39.37Z" }, - { url = "https://files.pythonhosted.org/packages/97/2d/7b54f80b93379ff94afb3bd9b0cd1d17b48183a0d6f98045bc01ce1e06a7/pandas-2.2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58b84b91b0b9f4bafac2a0ac55002280c094dfc6402402332c0913a59654ab2b", size = 15602548, upload-time = "2024-04-10T19:44:42.902Z" }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4d82be566f069d7a9a702dcdf6f9106df0e0b042e738043c0cc7ddd7e3f6/pandas-2.2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2123dc9ad6a814bcdea0f099885276b31b24f7edf40f6cdbc0912672e22eee", size = 13031332, upload-time = "2024-04-10T19:44:46.98Z" }, - { url = "https://files.pythonhosted.org/packages/92/a2/b79c48f530673567805e607712b29814b47dcaf0d167e87145eb4b0118c6/pandas-2.2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2925720037f06e89af896c70bca73459d7e6a4be96f9de79e2d440bd499fe0db", size = 16286054, upload-time = "2024-04-10T19:44:50.51Z" }, - { url = "https://files.pythonhosted.org/packages/40/c7/47e94907f1d8fdb4868d61bd6c93d57b3784a964d52691b77ebfdb062842/pandas-2.2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0cace394b6ea70c01ca1595f839cf193df35d1575986e484ad35c4aeae7266c1", size = 13879507, upload-time = "2024-04-10T19:44:54.412Z" }, - { url = "https://files.pythonhosted.org/packages/ab/63/966db1321a0ad55df1d1fe51505d2cdae191b84c907974873817b0a6e849/pandas-2.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:873d13d177501a28b2756375d59816c365e42ed8417b41665f346289adc68d24", size = 11634249, upload-time = "2024-04-10T19:44:58.183Z" }, - { url = "https://files.pythonhosted.org/packages/dd/49/de869130028fb8d90e25da3b7d8fb13e40f5afa4c4af1781583eb1ff3839/pandas-2.2.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9dfde2a0ddef507a631dc9dc4af6a9489d5e2e740e226ad426a05cabfbd7c8ef", size = 12500886, upload-time = "2024-04-10T19:45:01.808Z" }, - { url = "https://files.pythonhosted.org/packages/db/7c/9a60add21b96140e22465d9adf09832feade45235cd22f4cb1668a25e443/pandas-2.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b79011ff7a0f4b1d6da6a61aa1aa604fb312d6647de5bad20013682d1429ce", size = 11340320, upload-time = "2024-04-11T18:36:14.398Z" }, - { url = "https://files.pythonhosted.org/packages/b0/85/f95b5f322e1ae13b7ed7e97bd999160fa003424711ab4dc8344b8772c270/pandas-2.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb51fe389360f3b5a4d57dbd2848a5f033350336ca3b340d1c53a1fad33bcad", size = 15204346, upload-time = "2024-04-10T19:45:05.903Z" }, - { url = "https://files.pythonhosted.org/packages/40/10/79e52ef01dfeb1c1ca47a109a01a248754ebe990e159a844ece12914de83/pandas-2.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eee3a87076c0756de40b05c5e9a6069c035ba43e8dd71c379e68cab2c20f16ad", size = 12733396, upload-time = "2024-04-10T19:45:09.282Z" }, - { url = "https://files.pythonhosted.org/packages/35/9d/208febf8c4eb5c1d9ea3314d52d8bd415fd0ef0dd66bb24cc5bdbc8fa71a/pandas-2.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3e374f59e440d4ab45ca2fffde54b81ac3834cf5ae2cdfa69c90bc03bde04d76", size = 15858913, upload-time = "2024-04-10T19:45:12.514Z" }, - { url = "https://files.pythonhosted.org/packages/99/d1/2d9bd05def7a9e08a92ec929b5a4c8d5556ec76fae22b0fa486cbf33ea63/pandas-2.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:43498c0bdb43d55cb162cdc8c06fac328ccb5d2eabe3cadeb3529ae6f0517c32", size = 13417786, upload-time = "2024-04-10T19:45:16.275Z" }, - { url = "https://files.pythonhosted.org/packages/22/a5/a0b255295406ed54269814bc93723cfd1a0da63fb9aaf99e1364f07923e5/pandas-2.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:d187d355ecec3629624fccb01d104da7d7f391db0311145817525281e2804d23", size = 11498828, upload-time = "2024-04-10T19:45:19.85Z" }, -] - -[[package]] -name = "pandocfilters" -version = "1.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, -] - -[[package]] -name = "parso" -version = "0.8.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" }, -] - -[[package]] -name = "pathable" -version = "0.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" }, -] - -[[package]] -name = "pexpect" -version = "4.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ptyprocess" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, -] - -[[package]] -name = "pickleshare" -version = "0.7.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/b6/df3c1c9b616e9c0edbc4fbab6ddd09df9535849c64ba51fcb6531c32d4d8/pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca", size = 6161, upload-time = "2018-09-25T19:17:37.249Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/41/220f49aaea88bc6fa6cba8d05ecf24676326156c23b991e80b3f2fc24c77/pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56", size = 6877, upload-time = "2018-09-25T19:17:35.817Z" }, -] - -[[package]] -name = "pillow" -version = "10.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/43/c50c17c5f7d438e836c169e343695534c38c77f60e7c90389bd77981bc21/pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d", size = 46572854, upload-time = "2024-04-01T12:19:40.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/51/e4b35e394b4e5ca24983e50361a1db3d7da05b1758074f9c4f5b4be4b22a/pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795", size = 3528936, upload-time = "2024-04-01T12:17:29.322Z" }, - { url = "https://files.pythonhosted.org/packages/00/5c/7633f291def20082bad31b844fe5ed07742aae8504e4cfe2f331ee727178/pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57", size = 3352899, upload-time = "2024-04-01T12:17:31.843Z" }, - { url = "https://files.pythonhosted.org/packages/1d/29/abda81a079cccd1840b0b7b13ad67ffac87cc66395ae20973027280e9f9f/pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27", size = 4317733, upload-time = "2024-04-01T12:17:34.494Z" }, - { url = "https://files.pythonhosted.org/packages/77/cd/5205fb43a6000d424291b0525b8201004700d9a34e034517ac4dfdc6eed5/pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994", size = 4429430, upload-time = "2024-04-01T12:17:37.112Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bb/9e8d2b1b54235bd44139ee387beeb65ad9d8d755b5c01f817070c6dabea7/pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451", size = 4341711, upload-time = "2024-04-01T12:17:39.151Z" }, - { url = "https://files.pythonhosted.org/packages/81/ff/ad3c942d865f9e45ce84eeb31795e6d4d94e1f1eea51026d5154028510d7/pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd", size = 4507469, upload-time = "2024-04-01T12:17:41.159Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ab/30cd50a12d9afa2c412efcb8b37dd3f5f1da4bc77b984ddfbc776d96cf5b/pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad", size = 4533491, upload-time = "2024-04-01T12:17:43.813Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f0/07419615ffa852cded35dfa3337bf70788f232a3dfe622b97d5eb0c32674/pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c", size = 4598334, upload-time = "2024-04-01T12:17:46.271Z" }, - { url = "https://files.pythonhosted.org/packages/9c/f3/6e923786f2b2d167d16783fc079c003aadbcedc4995f54e8429d91aabfc4/pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09", size = 2217293, upload-time = "2024-04-01T12:17:48.292Z" }, - { url = "https://files.pythonhosted.org/packages/0a/16/c83877524c47976f16703d2e05c363244bc1e60ab439e078b3cd046d07db/pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d", size = 2531332, upload-time = "2024-04-01T12:17:50.844Z" }, - { url = "https://files.pythonhosted.org/packages/a8/3b/f64454549af90818774c3210b48987c3aeca5285787dbd69869d9a05b58f/pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f", size = 2229546, upload-time = "2024-04-01T12:17:53.237Z" }, - { url = "https://files.pythonhosted.org/packages/cc/5d/b7fcd38cba0f7706f64c1674fc9f018e4c64f791770598c44affadea7c2f/pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84", size = 3528535, upload-time = "2024-04-01T12:17:55.891Z" }, - { url = "https://files.pythonhosted.org/packages/5e/77/4cf407e7b033b4d8e5fcaac295b6e159cf1c70fa105d769f01ea2e1e5eca/pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19", size = 3352281, upload-time = "2024-04-01T12:17:58.527Z" }, - { url = "https://files.pythonhosted.org/packages/53/7b/4f7b153a776725a87797d744ea1c73b83ac0b723f5e379297605dee118eb/pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338", size = 4321427, upload-time = "2024-04-01T12:18:00.809Z" }, - { url = "https://files.pythonhosted.org/packages/45/08/d2cc751b790e77464f8648aa707e2327d6da5d95cf236a532e99c2e7a499/pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1", size = 4435915, upload-time = "2024-04-01T12:18:03.084Z" }, - { url = "https://files.pythonhosted.org/packages/ef/97/f69d1932cf45bf5bd9fa1e2ae57bdf716524faa4fa9fb7dc62cdb1a19113/pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462", size = 4347392, upload-time = "2024-04-01T12:18:05.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/c1/3521ddb9c1f3ac106af3e4512a98c785b6ed8a39e0f778480b8a4d340165/pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a", size = 4514536, upload-time = "2024-04-01T12:18:08.039Z" }, - { url = "https://files.pythonhosted.org/packages/c0/6f/347c241904a6514e59515284b01ba6f61765269a0d1a19fd2e6cbe331c8a/pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef", size = 4555987, upload-time = "2024-04-01T12:18:10.106Z" }, - { url = "https://files.pythonhosted.org/packages/c3/e2/3cc490c6b2e262713da82ce849c34bd8e6c31242afb53be8595d820b9877/pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3", size = 4623526, upload-time = "2024-04-01T12:18:12.172Z" }, - { url = "https://files.pythonhosted.org/packages/c1/b3/0209f70fa29b383e7618e47db95712a45788dea03bb960601753262a2883/pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d", size = 2217547, upload-time = "2024-04-01T12:18:14.188Z" }, - { url = "https://files.pythonhosted.org/packages/d3/23/3927d888481ff7c44fdbca3bc2a2e97588c933db46723bf115201377c436/pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b", size = 2531641, upload-time = "2024-04-01T12:18:16.081Z" }, - { url = "https://files.pythonhosted.org/packages/db/36/1ecaa0541d3a1b1362f937d386eeb1875847bfa06d5225f1b0e1588d1007/pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a", size = 2229746, upload-time = "2024-04-01T12:18:18.174Z" }, -] - -[[package]] -name = "pipreqs" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docopt" }, - { name = "ipython" }, - { name = "nbconvert" }, - { name = "yarg" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/12/4c/0a335b1b70c7e1821140ac6f884b51d47f049bcb600fa19bb374922f73aa/pipreqs-0.5.0.tar.gz", hash = "sha256:f33298d235ff76def369cb9a3594d084d9badc70cebba1e8cb271fcd4fdc0183", size = 35240, upload-time = "2024-02-18T17:49:36.607Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/38/cc1343c3a63655e18328e51e00c6e6851be648f1b8babffc5131f1b9f226/pipreqs-0.5.0-py3-none-any.whl", hash = "sha256:0809f6217028e35785f80e90217e18043e58c99ba28175e28320f9074dd03874", size = 33496, upload-time = "2024-02-18T17:49:34.148Z" }, -] - -[[package]] -name = "platformdirs" -version = "4.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "prance" -version = "23.6.21.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "chardet" }, - { name = "packaging" }, - { name = "requests" }, - { name = "ruamel-yaml" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/f0/bcb5ffc8b7ab8e3d02dbef3bd945cf8fd6e12c146774f900659406b9fce1/prance-23.6.21.0.tar.gz", hash = "sha256:d8c15f8ac34019751cc4945f866d8d964d7888016d10de3592e339567177cabe", size = 2798776, upload-time = "2023-06-21T20:01:57.142Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/db/4fb4901ee61274d0ab97746461fc5f2637e5d73aa73f34ee28e941a699a1/prance-23.6.21.0-py3-none-any.whl", hash = "sha256:6a4276fa07ed9f22feda4331097d7503c4adc3097e46ffae97425f2c1026bd9f", size = 36279, upload-time = "2023-06-21T20:01:54.936Z" }, -] - -[[package]] -name = "pre-commit" -version = "3.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cfgv" }, - { name = "identify" }, - { name = "nodeenv" }, - { name = "pyyaml" }, - { name = "virtualenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/46/cc214ef6514270328910083d0119d0a80a6d2c4ec8c6608c0219db0b74cf/pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a", size = 177317, upload-time = "2024-05-11T01:25:19.473Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/0f/d6d0b4e2f5b2933a557087fc0560371aa545a18232d4d3427eb3bb3af12e/pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5", size = 204268, upload-time = "2024-05-11T01:25:16.845Z" }, -] - -[[package]] -name = "prompt-toolkit" -version = "3.0.52" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "wcwidth" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, -] - -[[package]] -name = "psutil" -version = "5.9.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/90/c7/6dc0a455d111f68ee43f27793971cf03fe29b6ef972042549db29eec39a2/psutil-5.9.8.tar.gz", hash = "sha256:6be126e3225486dff286a8fb9a06246a5253f4c7c53b475ea5f5ac934e64194c", size = 503247, upload-time = "2024-01-19T20:47:09.517Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/e3/07ae864a636d70a8a6f58da27cb1179192f1140d5d1da10886ade9405797/psutil-5.9.8-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:aee678c8720623dc456fa20659af736241f575d79429a0e5e9cf88ae0605cc81", size = 248702, upload-time = "2024-01-19T20:47:36.303Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bd/28c5f553667116b2598b9cc55908ec435cb7f77a34f2bff3e3ca765b0f78/psutil-5.9.8-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb6403ce6d8e047495a701dc7c5bd788add903f8986d523e3e20b98b733e421", size = 285242, upload-time = "2024-01-19T20:47:39.65Z" }, - { url = "https://files.pythonhosted.org/packages/c5/4f/0e22aaa246f96d6ac87fe5ebb9c5a693fbe8877f537a1022527c47ca43c5/psutil-5.9.8-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d06016f7f8625a1825ba3732081d77c94589dca78b7a3fc072194851e88461a4", size = 288191, upload-time = "2024-01-19T20:47:43.078Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/2aa3a4acdc1e5940b59d421742356f133185667dd190b166dbcfcf5d7b43/psutil-5.9.8-cp37-abi3-win32.whl", hash = "sha256:bc56c2a1b0d15aa3eaa5a60c9f3f8e3e565303b465dbf57a1b730e7a2b9844e0", size = 251252, upload-time = "2024-01-19T20:47:52.88Z" }, - { url = "https://files.pythonhosted.org/packages/93/52/3e39d26feae7df0aa0fd510b14012c3678b36ed068f7d78b8d8784d61f0e/psutil-5.9.8-cp37-abi3-win_amd64.whl", hash = "sha256:8db4c1b57507eef143a15a6884ca10f7c73876cdf5d51e713151c1236a0e68cf", size = 255090, upload-time = "2024-01-19T20:47:56.019Z" }, - { url = "https://files.pythonhosted.org/packages/05/33/2d74d588408caedd065c2497bdb5ef83ce6082db01289a1e1147f6639802/psutil-5.9.8-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:d16bbddf0693323b8c6123dd804100241da461e41d6e332fb0ba6058f630f8c8", size = 249898, upload-time = "2024-01-19T20:47:59.238Z" }, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, -] - -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, - { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, - { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, - { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, - { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, - { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, - { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, - { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, - { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { url = "https://files.pythonhosted.org/packages/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" }, - { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, - { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, - { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, - { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, - { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, - { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, - { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184, upload-time = "2025-11-10T14:25:47.013Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, -] - -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pymupdf" -version = "1.26.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/48/d6/09b28f027b510838559f7748807192149c419b30cb90e6d5f0cf916dc9dc/pymupdf-1.26.7.tar.gz", hash = "sha256:71add8bdc8eb1aaa207c69a13400693f06ad9b927bea976f5d5ab9df0bb489c3", size = 84327033, upload-time = "2025-12-11T21:48:50.694Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/35/cd74cea1787b2247702ef8522186bdef32e9cb30a099e6bb864627ef6045/pymupdf-1.26.7-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:07085718dfdae5ab83b05eb5eb397f863bcc538fe05135318a01ea353e7a1353", size = 23179369, upload-time = "2025-12-11T21:47:21.587Z" }, - { url = "https://files.pythonhosted.org/packages/72/74/448b6172927c829c6a3fba80078d7b0a016ebbe2c9ee528821f5ea21677a/pymupdf-1.26.7-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:31aa9c8377ea1eea02934b92f4dcf79fb2abba0bf41f8a46d64c3e31546a3c02", size = 22470101, upload-time = "2025-12-11T21:47:37.105Z" }, - { url = "https://files.pythonhosted.org/packages/65/e7/47af26f3ac76be7ac3dd4d6cc7ee105948a8355d774e5ca39857bf91c11c/pymupdf-1.26.7-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e419b609996434a14a80fa060adec72c434a1cca6a511ec54db9841bc5d51b3c", size = 23502486, upload-time = "2025-12-12T09:51:25.824Z" }, - { url = "https://files.pythonhosted.org/packages/2a/6b/3de1714d734ff949be1e90a22375d0598d3540b22ae73eb85c2d7d1f36a9/pymupdf-1.26.7-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:69dfc78f206a96e5b3ac22741263ebab945fdf51f0dbe7c5757c3511b23d9d72", size = 24115727, upload-time = "2025-12-11T21:47:51.274Z" }, - { url = "https://files.pythonhosted.org/packages/62/9b/f86224847949577a523be2207315ae0fd3155b5d909cd66c274d095349a3/pymupdf-1.26.7-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1d5106f46e1ca0d64d46bd51892372a4f82076bdc14a9678d33d630702abca36", size = 24324386, upload-time = "2025-12-12T14:58:45.483Z" }, - { url = "https://files.pythonhosted.org/packages/85/8e/a117d39092ca645fde8b903f4a941d9aa75b370a67b4f1f435f56393dc5a/pymupdf-1.26.7-cp310-abi3-win32.whl", hash = "sha256:7c9645b6f5452629c747690190350213d3e5bbdb6b2eca227d82702b327f6eee", size = 17203888, upload-time = "2025-12-12T13:59:57.613Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c3/d0047678146c294469c33bae167c8ace337deafb736b0bf97b9bc481aa65/pymupdf-1.26.7-cp310-abi3-win_amd64.whl", hash = "sha256:425b1befe40d41b72eb0fe211711c7ae334db5eb60307e9dd09066ed060cceba", size = 18405952, upload-time = "2025-12-11T21:48:02.947Z" }, -] - -[[package]] -name = "pypdf" -version = "6.4.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/c2/b59b02ff7f2dc006799d2c5dc3a8877686890abdd915176ef799070edf17/pypdf-6.4.2.tar.gz", hash = "sha256:c466ff1272ffb4712c2348d2bbc3019bc93f1c62ccfaf50808e3b9f13c3dc527", size = 5275502, upload-time = "2025-12-14T14:30:58.58Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/99/3147435e15ccd97c0451efc3d13495dc22602e9887f81e64f1b135bae821/pypdf-6.4.2-py3-none-any.whl", hash = "sha256:014dcff867fd99fc0b6fc90ed1f7e1347ef2317ae038a489c2caa64106d268f4", size = 328212, upload-time = "2025-12-14T14:30:56.701Z" }, -] - -[[package]] -name = "pytest" -version = "8.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/9d/78b3785134306efe9329f40815af45b9215068d6ae4747ec0bc91ff1f4aa/pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f", size = 1422883, upload-time = "2024-04-27T23:34:55.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/43/6b1debd95ecdf001bc46789a933f658da3f9738c65f32db3f4e8f2a4ca97/pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233", size = 339229, upload-time = "2024-04-27T23:34:52.413Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "0.23.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cd/ef/80107b9e939875ad613c705d99d91e4510dcf5fed29613ac9aecbcba0a8d/pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f", size = 46203, upload-time = "2024-03-19T07:17:34.888Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/c9/de22c040d4c821c6c797ca1d720f1f4b2f4293d5757e811c62ae544496c4/pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a", size = 17552, upload-time = "2024-03-19T07:17:32.951Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" }, -] - -[[package]] -name = "python-iso639" -version = "2025.11.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/3b/3e07aadeeb7bbb2574d6aa6ccacbc58b17bd2b1fb6c7196bf96ab0e45129/python_iso639-2025.11.16.tar.gz", hash = "sha256:aabe941267898384415a509f5236d7cfc191198c84c5c6f73dac73d9783f5169", size = 174186, upload-time = "2025-11-16T21:53:37.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/2d/563849c31e58eb2e273fa0c391a7d9987db32f4d9152fe6ecdac0a8ffe93/python_iso639-2025.11.16-py3-none-any.whl", hash = "sha256:65f6ac6c6d8e8207f6175f8bf7fff7db486c6dc5c1d8866c2b77d2a923370896", size = 167818, upload-time = "2025-11-16T21:53:35.36Z" }, -] - -[[package]] -name = "python-magic" -version = "0.4.27" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/db/0b3e28ac047452d079d375ec6798bf76a036a08182dbb39ed38116a49130/python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b", size = 14677, upload-time = "2022-06-07T20:16:59.508Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/73/9f872cb81fc5c3bb48f7227872c28975f998f3e7c2b1c16e95e6432bbb90/python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3", size = 13840, upload-time = "2022-06-07T20:16:57.763Z" }, -] - -[[package]] -name = "python-multipart" -version = "0.0.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196, upload-time = "2025-12-17T09:24:22.446Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541, upload-time = "2025-12-17T09:24:21.153Z" }, -] - -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, - { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, - { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, - { 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" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, -] - -[[package]] -name = "pyzmq" -version = "27.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "implementation_name == 'pypy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/06/5d/305323ba86b284e6fcb0d842d6adaa2999035f70f8c38a9b6d21ad28c3d4/pyzmq-27.1.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:226b091818d461a3bef763805e75685e478ac17e9008f49fce2d3e52b3d58b86", size = 1333328, upload-time = "2025-09-08T23:07:45.946Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a0/fc7e78a23748ad5443ac3275943457e8452da67fda347e05260261108cbc/pyzmq-27.1.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0790a0161c281ca9723f804871b4027f2e8b5a528d357c8952d08cd1a9c15581", size = 908803, upload-time = "2025-09-08T23:07:47.551Z" }, - { url = "https://files.pythonhosted.org/packages/7e/22/37d15eb05f3bdfa4abea6f6d96eb3bb58585fbd3e4e0ded4e743bc650c97/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c895a6f35476b0c3a54e3eb6ccf41bf3018de937016e6e18748317f25d4e925f", size = 668836, upload-time = "2025-09-08T23:07:49.436Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c4/2a6fe5111a01005fc7af3878259ce17684fabb8852815eda6225620f3c59/pyzmq-27.1.0-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bbf8d3630bf96550b3be8e1fc0fea5cbdc8d5466c1192887bd94869da17a63e", size = 857038, upload-time = "2025-09-08T23:07:51.234Z" }, - { url = "https://files.pythonhosted.org/packages/cb/eb/bfdcb41d0db9cd233d6fb22dc131583774135505ada800ebf14dfb0a7c40/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15c8bd0fe0dabf808e2d7a681398c4e5ded70a551ab47482067a572c054c8e2e", size = 1657531, upload-time = "2025-09-08T23:07:52.795Z" }, - { url = "https://files.pythonhosted.org/packages/ab/21/e3180ca269ed4a0de5c34417dfe71a8ae80421198be83ee619a8a485b0c7/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bafcb3dd171b4ae9f19ee6380dfc71ce0390fefaf26b504c0e5f628d7c8c54f2", size = 2034786, upload-time = "2025-09-08T23:07:55.047Z" }, - { url = "https://files.pythonhosted.org/packages/3b/b1/5e21d0b517434b7f33588ff76c177c5a167858cc38ef740608898cd329f2/pyzmq-27.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e829529fcaa09937189178115c49c504e69289abd39967cd8a4c215761373394", size = 1894220, upload-time = "2025-09-08T23:07:57.172Z" }, - { url = "https://files.pythonhosted.org/packages/03/f2/44913a6ff6941905efc24a1acf3d3cb6146b636c546c7406c38c49c403d4/pyzmq-27.1.0-cp311-cp311-win32.whl", hash = "sha256:6df079c47d5902af6db298ec92151db82ecb557af663098b92f2508c398bb54f", size = 567155, upload-time = "2025-09-08T23:07:59.05Z" }, - { url = "https://files.pythonhosted.org/packages/23/6d/d8d92a0eb270a925c9b4dd039c0b4dc10abc2fcbc48331788824ef113935/pyzmq-27.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:190cbf120fbc0fc4957b56866830def56628934a9d112aec0e2507aa6a032b97", size = 633428, upload-time = "2025-09-08T23:08:00.663Z" }, - { url = "https://files.pythonhosted.org/packages/ae/14/01afebc96c5abbbd713ecfc7469cfb1bc801c819a74ed5c9fad9a48801cb/pyzmq-27.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:eca6b47df11a132d1745eb3b5b5e557a7dae2c303277aa0e69c6ba91b8736e07", size = 559497, upload-time = "2025-09-08T23:08:02.15Z" }, - { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" }, - { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" }, - { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" }, - { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" }, - { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" }, - { 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/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" }, - { url = "https://files.pythonhosted.org/packages/a1/cf/f2b3784d536250ffd4be70e049f3b60981235d70c6e8ce7e3ef21e1adb25/pyzmq-27.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f605d884e7c8be8fe1aa94e0a783bf3f591b84c24e4bc4f3e7564c82ac25e271", size = 747371, upload-time = "2025-09-08T23:09:54.563Z" }, - { url = "https://files.pythonhosted.org/packages/01/1b/5dbe84eefc86f48473947e2f41711aded97eecef1231f4558f1f02713c12/pyzmq-27.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c9f7f6e13dff2e44a6afeaf2cf54cee5929ad64afaf4d40b50f93c58fc687355", size = 544862, upload-time = "2025-09-08T23:09:56.509Z" }, -] - -[[package]] -name = "rapidfuzz" -version = "3.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, - { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, - { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, - { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" }, - { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" }, - { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" }, - { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" }, - { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" }, - { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" }, - { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" }, - { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" }, - { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" }, - { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" }, - { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" }, - { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" }, - { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" }, - { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" }, - { 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/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" }, - { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" }, -] - -[[package]] -name = "referencing" -version = "0.36.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, -] - -[[package]] -name = "regex" -version = "2025.11.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669, upload-time = "2025-11-03T21:34:22.089Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/90/4fb5056e5f03a7048abd2b11f598d464f0c167de4f2a51aa868c376b8c70/regex-2025.11.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eadade04221641516fa25139273505a1c19f9bf97589a05bc4cfcd8b4a618031", size = 488081, upload-time = "2025-11-03T21:31:11.946Z" }, - { url = "https://files.pythonhosted.org/packages/85/23/63e481293fac8b069d84fba0299b6666df720d875110efd0338406b5d360/regex-2025.11.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:feff9e54ec0dd3833d659257f5c3f5322a12eee58ffa360984b716f8b92983f4", size = 290554, upload-time = "2025-11-03T21:31:13.387Z" }, - { url = "https://files.pythonhosted.org/packages/2b/9d/b101d0262ea293a0066b4522dfb722eb6a8785a8c3e084396a5f2c431a46/regex-2025.11.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3b30bc921d50365775c09a7ed446359e5c0179e9e2512beec4a60cbcef6ddd50", size = 288407, upload-time = "2025-11-03T21:31:14.809Z" }, - { url = "https://files.pythonhosted.org/packages/0c/64/79241c8209d5b7e00577ec9dca35cd493cc6be35b7d147eda367d6179f6d/regex-2025.11.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f99be08cfead2020c7ca6e396c13543baea32343b7a9a5780c462e323bd8872f", size = 793418, upload-time = "2025-11-03T21:31:16.556Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e2/23cd5d3573901ce8f9757c92ca4db4d09600b865919b6d3e7f69f03b1afd/regex-2025.11.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6dd329a1b61c0ee95ba95385fb0c07ea0d3fe1a21e1349fa2bec272636217118", size = 860448, upload-time = "2025-11-03T21:31:18.12Z" }, - { url = "https://files.pythonhosted.org/packages/2a/4c/aecf31beeaa416d0ae4ecb852148d38db35391aac19c687b5d56aedf3a8b/regex-2025.11.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4c5238d32f3c5269d9e87be0cf096437b7622b6920f5eac4fd202468aaeb34d2", size = 907139, upload-time = "2025-11-03T21:31:20.753Z" }, - { url = "https://files.pythonhosted.org/packages/61/22/b8cb00df7d2b5e0875f60628594d44dba283e951b1ae17c12f99e332cc0a/regex-2025.11.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10483eefbfb0adb18ee9474498c9a32fcf4e594fbca0543bb94c48bac6183e2e", size = 800439, upload-time = "2025-11-03T21:31:22.069Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/c4b20330a5cdc7a8eb265f9ce593f389a6a88a0c5f280cf4d978f33966bc/regex-2025.11.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78c2d02bb6e1da0720eedc0bad578049cad3f71050ef8cd065ecc87691bed2b0", size = 782965, upload-time = "2025-11-03T21:31:23.598Z" }, - { url = "https://files.pythonhosted.org/packages/b4/4c/ae3e52988ae74af4b04d2af32fee4e8077f26e51b62ec2d12d246876bea2/regex-2025.11.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e6b49cd2aad93a1790ce9cffb18964f6d3a4b0b3dbdbd5de094b65296fce6e58", size = 854398, upload-time = "2025-11-03T21:31:25.008Z" }, - { url = "https://files.pythonhosted.org/packages/06/d1/a8b9cf45874eda14b2e275157ce3b304c87e10fb38d9fc26a6e14eb18227/regex-2025.11.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:885b26aa3ee56433b630502dc3d36ba78d186a00cc535d3806e6bfd9ed3c70ab", size = 845897, upload-time = "2025-11-03T21:31:26.427Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fe/1830eb0236be93d9b145e0bd8ab499f31602fe0999b1f19e99955aa8fe20/regex-2025.11.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ddd76a9f58e6a00f8772e72cff8ebcff78e022be95edf018766707c730593e1e", size = 788906, upload-time = "2025-11-03T21:31:28.078Z" }, - { url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" }, - { url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" }, - { url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" }, - { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" }, - { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" }, - { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" }, - { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" }, - { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" }, - { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" }, - { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" }, - { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" }, - { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" }, - { 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" }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, -] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, -] - -[[package]] -name = "rfc3339-validator" -version = "0.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" }, - { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" }, - { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" }, - { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" }, - { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" }, - { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" }, - { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" }, - { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" }, - { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" }, - { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" }, - { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" }, - { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" }, - { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" }, - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" }, - { url = "https://files.pythonhosted.org/packages/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" }, - { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" }, - { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" }, - { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" }, - { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" }, - { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" }, - { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" }, - { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" }, - { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" }, -] - -[[package]] -name = "ruamel-yaml" -version = "0.18.17" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ruamel-yaml-clib", marker = "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 = [ - { url = "https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl", hash = "sha256:9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d", size = 121594, upload-time = "2025-12-17T20:02:07.657Z" }, -] - -[[package]] -name = "ruamel-yaml-clib" -version = "0.2.15" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ea/97/60fda20e2fb54b83a61ae14648b0817c8f5d84a3821e40bfbdae1437026a/ruamel_yaml_clib-0.2.15.tar.gz", hash = "sha256:46e4cc8c43ef6a94885f72512094e482114a8a706d3c555a34ed4b0d20200600", size = 225794, upload-time = "2025-11-16T16:12:59.761Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/80/8ce7b9af532aa94dd83360f01ce4716264db73de6bc8efd22c32341f6658/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c583229f336682b7212a43d2fa32c30e643d3076178fb9f7a6a14dde85a2d8bd", size = 147998, upload-time = "2025-11-16T16:13:13.241Z" }, - { url = "https://files.pythonhosted.org/packages/53/09/de9d3f6b6701ced5f276d082ad0f980edf08ca67114523d1b9264cd5e2e0/ruamel_yaml_clib-0.2.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56ea19c157ed8c74b6be51b5fa1c3aff6e289a041575f0556f66e5fb848bb137", size = 132743, upload-time = "2025-11-16T16:13:14.265Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f7/73a9b517571e214fe5c246698ff3ed232f1ef863c8ae1667486625ec688a/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5fea0932358e18293407feb921d4f4457db837b67ec1837f87074667449f9401", size = 731459, upload-time = "2025-11-16T20:22:44.338Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a2/0dc0013169800f1c331a6f55b1282c1f4492a6d32660a0cf7b89e6684919/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71831bd61fbdb7aa0399d5c4da06bea37107ab5c79ff884cc07f2450910262", size = 749289, upload-time = "2025-11-16T16:13:15.633Z" }, - { url = "https://files.pythonhosted.org/packages/aa/ed/3fb20a1a96b8dc645d88c4072df481fe06e0289e4d528ebbdcc044ebc8b3/ruamel_yaml_clib-0.2.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:617d35dc765715fa86f8c3ccdae1e4229055832c452d4ec20856136acc75053f", size = 777630, upload-time = "2025-11-16T16:13:16.898Z" }, - { url = "https://files.pythonhosted.org/packages/60/50/6842f4628bc98b7aa4733ab2378346e1441e150935ad3b9f3c3c429d9408/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b45498cc81a4724a2d42273d6cfc243c0547ad7c6b87b4f774cb7bcc131c98d", size = 744368, upload-time = "2025-11-16T16:13:18.117Z" }, - { url = "https://files.pythonhosted.org/packages/d3/b0/128ae8e19a7d794c2e36130a72b3bb650ce1dd13fb7def6cf10656437dcf/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:def5663361f6771b18646620fca12968aae730132e104688766cf8a3b1d65922", size = 745233, upload-time = "2025-11-16T20:22:45.833Z" }, - { url = "https://files.pythonhosted.org/packages/75/05/91130633602d6ba7ce3e07f8fc865b40d2a09efd4751c740df89eed5caf9/ruamel_yaml_clib-0.2.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:014181cdec565c8745b7cbc4de3bf2cc8ced05183d986e6d1200168e5bb59490", size = 770963, upload-time = "2025-11-16T16:13:19.344Z" }, - { url = "https://files.pythonhosted.org/packages/fd/4b/fd4542e7f33d7d1bc64cc9ac9ba574ce8cf145569d21f5f20133336cdc8c/ruamel_yaml_clib-0.2.15-cp311-cp311-win32.whl", hash = "sha256:d290eda8f6ada19e1771b54e5706b8f9807e6bb08e873900d5ba114ced13e02c", size = 102640, upload-time = "2025-11-16T16:13:20.498Z" }, - { url = "https://files.pythonhosted.org/packages/bb/eb/00ff6032c19c7537371e3119287999570867a0eafb0154fccc80e74bf57a/ruamel_yaml_clib-0.2.15-cp311-cp311-win_amd64.whl", hash = "sha256:bdc06ad71173b915167702f55d0f3f027fc61abd975bd308a0968c02db4a4c3e", size = 121996, upload-time = "2025-11-16T16:13:21.855Z" }, - { url = "https://files.pythonhosted.org/packages/72/4b/5fde11a0722d676e469d3d6f78c6a17591b9c7e0072ca359801c4bd17eee/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb15a2e2a90c8475df45c0949793af1ff413acfb0a716b8b94e488ea95ce7cff", size = 149088, upload-time = "2025-11-16T16:13:22.836Z" }, - { url = "https://files.pythonhosted.org/packages/85/82/4d08ac65ecf0ef3b046421985e66301a242804eb9a62c93ca3437dc94ee0/ruamel_yaml_clib-0.2.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64da03cbe93c1e91af133f5bec37fd24d0d4ba2418eaf970d7166b0a26a148a2", size = 134553, upload-time = "2025-11-16T16:13:24.151Z" }, - { url = "https://files.pythonhosted.org/packages/b9/cb/22366d68b280e281a932403b76da7a988108287adff2bfa5ce881200107a/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f6d3655e95a80325b84c4e14c080b2470fe4f33b6846f288379ce36154993fb1", size = 737468, upload-time = "2025-11-16T20:22:47.335Z" }, - { url = "https://files.pythonhosted.org/packages/71/73/81230babf8c9e33770d43ed9056f603f6f5f9665aea4177a2c30ae48e3f3/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71845d377c7a47afc6592aacfea738cc8a7e876d586dfba814501d8c53c1ba60", size = 753349, upload-time = "2025-11-16T16:13:26.269Z" }, - { url = "https://files.pythonhosted.org/packages/61/62/150c841f24cda9e30f588ef396ed83f64cfdc13b92d2f925bb96df337ba9/ruamel_yaml_clib-0.2.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e5499db1ccbc7f4b41f0565e4f799d863ea720e01d3e99fa0b7b5fcd7802c9", size = 788211, upload-time = "2025-11-16T16:13:27.441Z" }, - { url = "https://files.pythonhosted.org/packages/30/93/e79bd9cbecc3267499d9ead919bd61f7ddf55d793fb5ef2b1d7d92444f35/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4b293a37dc97e2b1e8a1aec62792d1e52027087c8eea4fc7b5abd2bdafdd6642", size = 743203, upload-time = "2025-11-16T16:13:28.671Z" }, - { url = "https://files.pythonhosted.org/packages/8d/06/1eb640065c3a27ce92d76157f8efddb184bd484ed2639b712396a20d6dce/ruamel_yaml_clib-0.2.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:512571ad41bba04eac7268fe33f7f4742210ca26a81fe0c75357fa682636c690", size = 747292, upload-time = "2025-11-16T20:22:48.584Z" }, - { 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" }, -] - -[[package]] -name = "safetensors" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, - { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, - { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, - { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, - { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, - { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, - { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, - { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, - { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, - { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, - { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, -] - -[[package]] -name = "scikit-learn" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "joblib" }, - { name = "numpy" }, - { name = "scipy" }, - { name = "threadpoolctl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c9/92/53ea2181da8ac6bf27170191028aee7251f8f841f8d3edbfdcaf2008fde9/scikit_learn-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da", size = 8595835, upload-time = "2025-12-10T07:07:39.385Z" }, - { url = "https://files.pythonhosted.org/packages/01/18/d154dc1638803adf987910cdd07097d9c526663a55666a97c124d09fb96a/scikit_learn-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1", size = 8080381, upload-time = "2025-12-10T07:07:41.93Z" }, - { url = "https://files.pythonhosted.org/packages/8a/44/226142fcb7b7101e64fdee5f49dbe6288d4c7af8abf593237b70fca080a4/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b", size = 8799632, upload-time = "2025-12-10T07:07:43.899Z" }, - { url = "https://files.pythonhosted.org/packages/36/4d/4a67f30778a45d542bbea5db2dbfa1e9e100bf9ba64aefe34215ba9f11f6/scikit_learn-1.8.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1", size = 9103788, upload-time = "2025-12-10T07:07:45.982Z" }, - { url = "https://files.pythonhosted.org/packages/89/3c/45c352094cfa60050bcbb967b1faf246b22e93cb459f2f907b600f2ceda5/scikit_learn-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b", size = 8081706, upload-time = "2025-12-10T07:07:48.111Z" }, - { url = "https://files.pythonhosted.org/packages/3d/46/5416595bb395757f754feb20c3d776553a386b661658fb21b7c814e89efe/scikit_learn-1.8.0-cp311-cp311-win_arm64.whl", hash = "sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961", size = 7688451, upload-time = "2025-12-10T07:07:49.873Z" }, - { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, - { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, - { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, - { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, - { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, - { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, -] - -[[package]] -name = "scipy" -version = "1.16.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0a/ca/d8ace4f98322d01abcd52d381134344bf7b431eba7ed8b42bdea5a3c2ac9/scipy-1.16.3.tar.gz", hash = "sha256:01e87659402762f43bd2fee13370553a17ada367d42e7487800bf2916535aecb", size = 30597883, upload-time = "2025-10-28T17:38:54.068Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/5f/6f37d7439de1455ce9c5a556b8d1db0979f03a796c030bafdf08d35b7bf9/scipy-1.16.3-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:40be6cf99e68b6c4321e9f8782e7d5ff8265af28ef2cd56e9c9b2638fa08ad97", size = 36630881, upload-time = "2025-10-28T17:31:47.104Z" }, - { url = "https://files.pythonhosted.org/packages/7c/89/d70e9f628749b7e4db2aa4cd89735502ff3f08f7b9b27d2e799485987cd9/scipy-1.16.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:8be1ca9170fcb6223cc7c27f4305d680ded114a1567c0bd2bfcbf947d1b17511", size = 28941012, upload-time = "2025-10-28T17:31:53.411Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a8/0e7a9a6872a923505dbdf6bb93451edcac120363131c19013044a1e7cb0c/scipy-1.16.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bea0a62734d20d67608660f69dcda23e7f90fb4ca20974ab80b6ed40df87a005", size = 20931935, upload-time = "2025-10-28T17:31:57.361Z" }, - { url = "https://files.pythonhosted.org/packages/bd/c7/020fb72bd79ad798e4dbe53938543ecb96b3a9ac3fe274b7189e23e27353/scipy-1.16.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:2a207a6ce9c24f1951241f4693ede2d393f59c07abc159b2cb2be980820e01fb", size = 23534466, upload-time = "2025-10-28T17:32:01.875Z" }, - { url = "https://files.pythonhosted.org/packages/be/a0/668c4609ce6dbf2f948e167836ccaf897f95fb63fa231c87da7558a374cd/scipy-1.16.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:532fb5ad6a87e9e9cd9c959b106b73145a03f04c7d57ea3e6f6bb60b86ab0876", size = 33593618, upload-time = "2025-10-28T17:32:06.902Z" }, - { url = "https://files.pythonhosted.org/packages/ca/6e/8942461cf2636cdae083e3eb72622a7fbbfa5cf559c7d13ab250a5dbdc01/scipy-1.16.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0151a0749efeaaab78711c78422d413c583b8cdd2011a3c1d6c794938ee9fdb2", size = 35899798, upload-time = "2025-10-28T17:32:12.665Z" }, - { url = "https://files.pythonhosted.org/packages/79/e8/d0f33590364cdbd67f28ce79368b373889faa4ee959588beddf6daef9abe/scipy-1.16.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7180967113560cca57418a7bc719e30366b47959dd845a93206fbed693c867e", size = 36226154, upload-time = "2025-10-28T17:32:17.961Z" }, - { url = "https://files.pythonhosted.org/packages/39/c1/1903de608c0c924a1749c590064e65810f8046e437aba6be365abc4f7557/scipy-1.16.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:deb3841c925eeddb6afc1e4e4a45e418d19ec7b87c5df177695224078e8ec733", size = 38878540, upload-time = "2025-10-28T17:32:23.907Z" }, - { url = "https://files.pythonhosted.org/packages/f1/d0/22ec7036ba0b0a35bccb7f25ab407382ed34af0b111475eb301c16f8a2e5/scipy-1.16.3-cp311-cp311-win_amd64.whl", hash = "sha256:53c3844d527213631e886621df5695d35e4f6a75f620dca412bcd292f6b87d78", size = 38722107, upload-time = "2025-10-28T17:32:29.921Z" }, - { url = "https://files.pythonhosted.org/packages/7b/60/8a00e5a524bb3bf8898db1650d350f50e6cffb9d7a491c561dc9826c7515/scipy-1.16.3-cp311-cp311-win_arm64.whl", hash = "sha256:9452781bd879b14b6f055b26643703551320aa8d79ae064a71df55c00286a184", size = 25506272, upload-time = "2025-10-28T17:32:34.577Z" }, - { url = "https://files.pythonhosted.org/packages/40/41/5bf55c3f386b1643812f3a5674edf74b26184378ef0f3e7c7a09a7e2ca7f/scipy-1.16.3-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:81fc5827606858cf71446a5e98715ba0e11f0dbc83d71c7409d05486592a45d6", size = 36659043, upload-time = "2025-10-28T17:32:40.285Z" }, - { url = "https://files.pythonhosted.org/packages/1e/0f/65582071948cfc45d43e9870bf7ca5f0e0684e165d7c9ef4e50d783073eb/scipy-1.16.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:c97176013d404c7346bf57874eaac5187d969293bf40497140b0a2b2b7482e07", size = 28898986, upload-time = "2025-10-28T17:32:45.325Z" }, - { url = "https://files.pythonhosted.org/packages/96/5e/36bf3f0ac298187d1ceadde9051177d6a4fe4d507e8f59067dc9dd39e650/scipy-1.16.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2b71d93c8a9936046866acebc915e2af2e292b883ed6e2cbe5c34beb094b82d9", size = 20889814, upload-time = "2025-10-28T17:32:49.277Z" }, - { url = "https://files.pythonhosted.org/packages/80/35/178d9d0c35394d5d5211bbff7ac4f2986c5488b59506fef9e1de13ea28d3/scipy-1.16.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3d4a07a8e785d80289dfe66b7c27d8634a773020742ec7187b85ccc4b0e7b686", size = 23565795, upload-time = "2025-10-28T17:32:53.337Z" }, - { url = "https://files.pythonhosted.org/packages/fa/46/d1146ff536d034d02f83c8afc3c4bab2eddb634624d6529a8512f3afc9da/scipy-1.16.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0553371015692a898e1aa858fed67a3576c34edefa6b7ebdb4e9dde49ce5c203", size = 33349476, upload-time = "2025-10-28T17:32:58.353Z" }, - { url = "https://files.pythonhosted.org/packages/79/2e/415119c9ab3e62249e18c2b082c07aff907a273741b3f8160414b0e9193c/scipy-1.16.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:72d1717fd3b5e6ec747327ce9bda32d5463f472c9dce9f54499e81fbd50245a1", size = 35676692, upload-time = "2025-10-28T17:33:03.88Z" }, - { url = "https://files.pythonhosted.org/packages/27/82/df26e44da78bf8d2aeaf7566082260cfa15955a5a6e96e6a29935b64132f/scipy-1.16.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1fb2472e72e24d1530debe6ae078db70fb1605350c88a3d14bc401d6306dbffe", size = 36019345, upload-time = "2025-10-28T17:33:09.773Z" }, - { 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" }, -] - -[[package]] -name = "sentence-transformers" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, - { name = "numpy" }, - { name = "pillow" }, - { name = "scikit-learn" }, - { name = "scipy" }, - { name = "torch" }, - { name = "tqdm" }, - { name = "transformers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/c0/7051c672a48fe561decf7208cc18bbbdd4efa3323873aa1c86a3fb77fd97/sentence_transformers-3.0.0.tar.gz", hash = "sha256:52d4101654ed107a28e9fa5110fce399084b55e7838fd8256471353ddc299033", size = 174658, upload-time = "2024-05-28T11:53:39.129Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/c4/99a9386808025d5a546576243bfd3b1eb669f978b8a0e05a1253eaf89bf0/sentence_transformers-3.0.0-py3-none-any.whl", hash = "sha256:9bf851b688b796e5fb06c920921efd5e5e05ee616e85cb3026fbdfe4dcf15bf3", size = 224681, upload-time = "2024-05-28T11:53:37.037Z" }, -] - -[[package]] -name = "setuptools" -version = "80.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "slack-sdk" -version = "3.31.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/e3/4a2491cbf793bb8da8a51120207df8c097faeda42bf720f7acf7c40e4ca8/slack_sdk-3.31.0.tar.gz", hash = "sha256:740d2f9c49cbfcbd46fca56b4be9d527934c225312aac18fd2c0fca0ef6bc935", size = 230928, upload-time = "2024-07-04T16:40:39.019Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/04/f4517d8403c49910f45ad91205de352f2eccf12ae28865a27da7d7d05bf6/slack_sdk-3.31.0-py2.py3-none-any.whl", hash = "sha256:a120cc461e8ebb7d9175f171dbe0ded37a6878d9f7b96b28e4bad1227399047b", size = 289845, upload-time = "2024-07-04T16:40:36.34Z" }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, -] - -[[package]] -name = "soupsieve" -version = "2.8" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6d/e6/21ccce3262dd4889aa3332e5a119a3491a95e8f60939870a3a035aabac0d/soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f", size = 103472, upload-time = "2025-08-27T15:39:51.78Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/a0/bb38d3b76b8cae341dad93a2dd83ab7462e6dbcdd84d43f54ee60a8dc167/soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c", size = 36679, upload-time = "2025-08-27T15:39:50.179Z" }, -] - -[[package]] -name = "sse-starlette" -version = "3.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/8b/54651ad49bce99a50fd61a7f19c2b6a79fbb072e693101fbb1194c362054/sse_starlette-3.0.4.tar.gz", hash = "sha256:5e34286862e96ead0eb70f5ddd0bd21ab1f6473a8f44419dd267f431611383dd", size = 22576, upload-time = "2025-12-14T16:22:52.493Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/22/8ab1066358601163e1ac732837adba3672f703818f693e179b24e0d3b65c/sse_starlette-3.0.4-py3-none-any.whl", hash = "sha256:32c80ef0d04506ced4b0b6ab8fe300925edc37d26f666afb1874c754895f5dc3", size = 11764, upload-time = "2025-12-14T16:22:51.453Z" }, -] - -[[package]] -name = "stack-data" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asttokens" }, - { name = "executing" }, - { name = "pure-eval" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, -] - -[[package]] -name = "starlette" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, -] - -[[package]] -name = "sympy" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mpmath" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, -] - -[[package]] -name = "tabulate" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, -] - -[[package]] -name = "texttable" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/dc/0aff23d6036a4d3bf4f1d8c8204c5c79c4437e25e0ae94ffe4bbb55ee3c2/texttable-1.7.0.tar.gz", hash = "sha256:2d2068fb55115807d3ac77a4ca68fa48803e84ebb0ee2340f858107a36522638", size = 12831, upload-time = "2023-10-03T09:48:12.272Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/99/4772b8e00a136f3e01236de33b0efda31ee7077203ba5967fcc76da94d65/texttable-1.7.0-py2.py3-none-any.whl", hash = "sha256:72227d592c82b3d7f672731ae73e4d1f88cd8e2ef5b075a7a7f01a23a3743917", size = 10768, upload-time = "2023-10-03T09:48:10.434Z" }, -] - -[[package]] -name = "threadpoolctl" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, -] - -[[package]] -name = "tiktoken" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "regex" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c4/4a/abaec53e93e3ef37224a4dd9e2fc6bb871e7a538c2b6b9d2a6397271daf4/tiktoken-0.7.0.tar.gz", hash = "sha256:1077266e949c24e0291f6c350433c6f0971365ece2b173a23bc3b9f9defef6b6", size = 33437, upload-time = "2024-05-13T18:03:28.793Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/22/eb/57492b2568eea1d546da5cc1ae7559d924275280db80ba07e6f9b89a914b/tiktoken-0.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:10c7674f81e6e350fcbed7c09a65bca9356eaab27fb2dac65a1e440f2bcfe30f", size = 961468, upload-time = "2024-05-13T18:02:43.788Z" }, - { url = "https://files.pythonhosted.org/packages/30/ef/e07dbfcb2f85c84abaa1b035a9279575a8da0236305491dc22ae099327f7/tiktoken-0.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:084cec29713bc9d4189a937f8a35dbdfa785bd1235a34c1124fe2323821ee93f", size = 907005, upload-time = "2024-05-13T18:02:45.327Z" }, - { url = "https://files.pythonhosted.org/packages/ea/9b/f36db825b1e9904c3a2646439cb9923fc1e09208e2e071c6d9dd64ead131/tiktoken-0.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:811229fde1652fedcca7c6dfe76724d0908775b353556d8a71ed74d866f73f7b", size = 1049183, upload-time = "2024-05-13T18:02:46.574Z" }, - { url = "https://files.pythonhosted.org/packages/61/b4/b80d1fe33015e782074e96bbbf4108ccd283b8deea86fb43c15d18b7c351/tiktoken-0.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b6e7dc2e7ad1b3757e8a24597415bafcfb454cebf9a33a01f2e6ba2e663992", size = 1080830, upload-time = "2024-05-13T18:02:48.444Z" }, - { url = "https://files.pythonhosted.org/packages/2a/40/c66ff3a21af6d62a7e0ff428d12002c4e0389f776d3ff96dcaa0bb354eee/tiktoken-0.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1063c5748be36344c7e18c7913c53e2cca116764c2080177e57d62c7ad4576d1", size = 1092967, upload-time = "2024-05-13T18:02:50.006Z" }, - { url = "https://files.pythonhosted.org/packages/2e/80/f4c9e255ff236e6a69ce44b927629cefc1b63d3a00e2d1c9ed540c9492d2/tiktoken-0.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20295d21419bfcca092644f7e2f2138ff947a6eb8cfc732c09cc7d76988d4a89", size = 1142682, upload-time = "2024-05-13T18:02:51.814Z" }, - { url = "https://files.pythonhosted.org/packages/b1/10/c04b4ff592a5f46b28ebf4c2353f735c02ae7f0ce1b165d00748ced6467e/tiktoken-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:959d993749b083acc57a317cbc643fb85c014d055b2119b739487288f4e5d1cb", size = 799009, upload-time = "2024-05-13T18:02:53.057Z" }, - { url = "https://files.pythonhosted.org/packages/1d/46/4cdda4186ce900608f522da34acf442363346688c71b938a90a52d7b84cc/tiktoken-0.7.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:71c55d066388c55a9c00f61d2c456a6086673ab7dec22dd739c23f77195b1908", size = 960446, upload-time = "2024-05-13T18:02:54.409Z" }, - { url = "https://files.pythonhosted.org/packages/b6/30/09ced367d280072d7a3e21f34263dfbbf6378661e7a0f6414e7c18971083/tiktoken-0.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:09ed925bccaa8043e34c519fbb2f99110bd07c6fd67714793c21ac298e449410", size = 906652, upload-time = "2024-05-13T18:02:56.25Z" }, - { url = "https://files.pythonhosted.org/packages/e6/7b/c949e4954441a879a67626963dff69096e3c774758b9f2bb0853f7b4e1e7/tiktoken-0.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03c6c40ff1db0f48a7b4d2dafeae73a5607aacb472fa11f125e7baf9dce73704", size = 1047904, upload-time = "2024-05-13T18:02:57.707Z" }, - { url = "https://files.pythonhosted.org/packages/50/81/1842a22f15586072280364c2ab1e40835adaf64e42fe80e52aff921ee021/tiktoken-0.7.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d20b5c6af30e621b4aca094ee61777a44118f52d886dbe4f02b70dfe05c15350", size = 1079836, upload-time = "2024-05-13T18:02:59.009Z" }, - { url = "https://files.pythonhosted.org/packages/6d/87/51a133a3d5307cf7ae3754249b0faaa91d3414b85c3d36f80b54d6817aa6/tiktoken-0.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d427614c3e074004efa2f2411e16c826f9df427d3c70a54725cae860f09e4bf4", size = 1092472, upload-time = "2024-05-13T18:03:00.597Z" }, - { url = "https://files.pythonhosted.org/packages/a5/1f/c93517dc6d3b2c9e988b8e24f87a8b2d4a4ab28920a3a3f3ea338397ae0c/tiktoken-0.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c46d7af7b8c6987fac9b9f61041b452afe92eb087d29c9ce54951280f899a97", size = 1141881, upload-time = "2024-05-13T18:03:02.743Z" }, - { url = "https://files.pythonhosted.org/packages/bf/4b/48ca098cb580c099b5058bf62c4cb5e90ca6130fa43ef4df27088536245b/tiktoken-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:0bc603c30b9e371e7c4c7935aba02af5994a909fc3c0fe66e7004070858d3f8f", size = 799281, upload-time = "2024-05-13T18:03:04.036Z" }, -] - -[[package]] -name = "tinycss2" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "webencodings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, -] - -[[package]] -name = "tokenizers" -version = "0.22.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "huggingface-hub" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, - { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, - { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, - { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, - { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, - { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, - { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, - { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, - { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, - { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, - { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, - { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, -] - -[[package]] -name = "torch" -version = "2.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "fsspec" }, - { name = "jinja2" }, - { name = "networkx" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "setuptools", marker = "python_full_version >= '3.12'" }, - { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/db/c064112ac0089af3d2f7a2b5bfbabf4aa407a78b74f87889e524b91c5402/torch-2.9.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:62b3fd888277946918cba4478cf849303da5359f0fb4e3bfb86b0533ba2eaf8d", size = 104220430, upload-time = "2025-11-12T15:20:31.705Z" }, - { url = "https://files.pythonhosted.org/packages/56/be/76eaa36c9cd032d3b01b001e2c5a05943df75f26211f68fae79e62f87734/torch-2.9.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d033ff0ac3f5400df862a51bdde9bad83561f3739ea0046e68f5401ebfa67c1b", size = 899821446, upload-time = "2025-11-12T15:20:15.544Z" }, - { url = "https://files.pythonhosted.org/packages/47/cc/7a2949e38dfe3244c4df21f0e1c27bce8aedd6c604a587dd44fc21017cb4/torch-2.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:0d06b30a9207b7c3516a9e0102114024755a07045f0c1d2f2a56b1819ac06bcb", size = 110973074, upload-time = "2025-11-12T15:21:39.958Z" }, - { url = "https://files.pythonhosted.org/packages/1e/ce/7d251155a783fb2c1bb6837b2b7023c622a2070a0a72726ca1df47e7ea34/torch-2.9.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:52347912d868653e1528b47cafaf79b285b98be3f4f35d5955389b1b95224475", size = 74463887, upload-time = "2025-11-12T15:20:36.611Z" }, - { url = "https://files.pythonhosted.org/packages/0f/27/07c645c7673e73e53ded71705045d6cb5bae94c4b021b03aa8d03eee90ab/torch-2.9.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:da5f6f4d7f4940a173e5572791af238cb0b9e21b1aab592bd8b26da4c99f1cd6", size = 104126592, upload-time = "2025-11-12T15:20:41.62Z" }, - { 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" }, -] - -[[package]] -name = "tornado" -version = "6.5.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/1d/0a336abf618272d53f62ebe274f712e213f5a03c0b2339575430b8362ef2/tornado-6.5.4.tar.gz", hash = "sha256:a22fa9047405d03260b483980635f0b041989d8bcc9a313f8fe18b411d84b1d7", size = 513632, upload-time = "2025-12-15T19:21:03.836Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/a9/e94a9d5224107d7ce3cc1fab8d5dc97f5ea351ccc6322ee4fb661da94e35/tornado-6.5.4-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d6241c1a16b1c9e4cc28148b1cda97dd1c6cb4fb7068ac1bedc610768dff0ba9", size = 443909, upload-time = "2025-12-15T19:20:48.382Z" }, - { url = "https://files.pythonhosted.org/packages/db/7e/f7b8d8c4453f305a51f80dbb49014257bb7d28ccb4bbb8dd328ea995ecad/tornado-6.5.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2d50f63dda1d2cac3ae1fa23d254e16b5e38153758470e9956cbc3d813d40843", size = 442163, upload-time = "2025-12-15T19:20:49.791Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b5/206f82d51e1bfa940ba366a8d2f83904b15942c45a78dd978b599870ab44/tornado-6.5.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1cf66105dc6acb5af613c054955b8137e34a03698aa53272dbda4afe252be17", size = 445746, upload-time = "2025-12-15T19:20:51.491Z" }, - { url = "https://files.pythonhosted.org/packages/8e/9d/1a3338e0bd30ada6ad4356c13a0a6c35fbc859063fa7eddb309183364ac1/tornado-6.5.4-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:50ff0a58b0dc97939d29da29cd624da010e7f804746621c78d14b80238669335", size = 445083, upload-time = "2025-12-15T19:20:52.778Z" }, - { url = "https://files.pythonhosted.org/packages/50/d4/e51d52047e7eb9a582da59f32125d17c0482d065afd5d3bc435ff2120dc5/tornado-6.5.4-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5fb5e04efa54cf0baabdd10061eb4148e0be137166146fff835745f59ab9f7f", size = 445315, upload-time = "2025-12-15T19:20:53.996Z" }, - { url = "https://files.pythonhosted.org/packages/27/07/2273972f69ca63dbc139694a3fc4684edec3ea3f9efabf77ed32483b875c/tornado-6.5.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9c86b1643b33a4cd415f8d0fe53045f913bf07b4a3ef646b735a6a86047dda84", size = 446003, upload-time = "2025-12-15T19:20:56.101Z" }, - { url = "https://files.pythonhosted.org/packages/d1/83/41c52e47502bf7260044413b6770d1a48dda2f0246f95ee1384a3cd9c44a/tornado-6.5.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:6eb82872335a53dd063a4f10917b3efd28270b56a33db69009606a0312660a6f", size = 445412, upload-time = "2025-12-15T19:20:57.398Z" }, - { url = "https://files.pythonhosted.org/packages/10/c7/bc96917f06cbee182d44735d4ecde9c432e25b84f4c2086143013e7b9e52/tornado-6.5.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6076d5dda368c9328ff41ab5d9dd3608e695e8225d1cd0fd1e006f05da3635a8", size = 445392, upload-time = "2025-12-15T19:20:58.692Z" }, - { url = "https://files.pythonhosted.org/packages/0c/1a/d7592328d037d36f2d2462f4bc1fbb383eec9278bc786c1b111cbbd44cfa/tornado-6.5.4-cp39-abi3-win32.whl", hash = "sha256:1768110f2411d5cd281bac0a090f707223ce77fd110424361092859e089b38d1", size = 446481, upload-time = "2025-12-15T19:21:00.008Z" }, - { url = "https://files.pythonhosted.org/packages/d6/6d/c69be695a0a64fd37a97db12355a035a6d90f79067a3cf936ec2b1dc38cd/tornado-6.5.4-cp39-abi3-win_amd64.whl", hash = "sha256:fa07d31e0cd85c60713f2b995da613588aa03e1303d75705dca6af8babc18ddc", size = 446886, upload-time = "2025-12-15T19:21:01.287Z" }, - { url = "https://files.pythonhosted.org/packages/50/49/8dc3fd90902f70084bd2cd059d576ddb4f8bb44c2c7c0e33a11422acb17e/tornado-6.5.4-cp39-abi3-win_arm64.whl", hash = "sha256:053e6e16701eb6cbe641f308f4c1a9541f91b6261991160391bfc342e8a551a1", size = 445910, upload-time = "2025-12-15T19:21:02.571Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, -] - -[[package]] -name = "traitlets" -version = "5.14.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, -] - -[[package]] -name = "transformers" -version = "4.57.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "filelock" }, - { name = "huggingface-hub" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "pyyaml" }, - { name = "regex" }, - { name = "requests" }, - { name = "safetensors" }, - { name = "tokenizers" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dd/70/d42a739e8dfde3d92bb2fff5819cbf331fe9657323221e79415cd5eb65ee/transformers-4.57.3.tar.gz", hash = "sha256:df4945029aaddd7c09eec5cad851f30662f8bd1746721b34cc031d70c65afebc", size = 10139680, upload-time = "2025-11-25T15:51:30.139Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/6b/2f416568b3c4c91c96e5a365d164f8a4a4a88030aa8ab4644181fdadce97/transformers-4.57.3-py3-none-any.whl", hash = "sha256:c77d353a4851b1880191603d36acb313411d3577f6e2897814f333841f7003f4", size = 11993463, upload-time = "2025-11-25T15:51:26.493Z" }, -] - -[[package]] -name = "triton" -version = "3.5.1" -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" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspect" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, -] - -[[package]] -name = "tzdata" -version = "2025.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, -] - -[[package]] -name = "unstructured" -version = "0.13.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backoff" }, - { name = "beautifulsoup4" }, - { name = "chardet" }, - { name = "dataclasses-json" }, - { name = "emoji" }, - { name = "filetype" }, - { name = "langdetect" }, - { name = "lxml" }, - { name = "nltk" }, - { name = "numpy" }, - { name = "python-iso639" }, - { name = "python-magic" }, - { name = "rapidfuzz" }, - { name = "requests" }, - { name = "tabulate" }, - { name = "typing-extensions" }, - { name = "unstructured-client" }, - { name = "wrapt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/e7/f3ff63814226e349a434dec7ede51f0e5af14eed325b3fd1c48be6fb8ff1/unstructured-0.13.7.tar.gz", hash = "sha256:5d59161d353b7006d8c6ee6f1a39154a5a11a5aaa258aac3fe90a8d44016aa6c", size = 1714285, upload-time = "2024-05-08T18:36:49.562Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f5/48/9bdd8aff34c507750a347545bcf26c025b1335c021ac0b9af5a542a8acd5/unstructured-0.13.7-py3-none-any.whl", hash = "sha256:a3d8f3037cb3063661531c6ecc04aca6df93c293ba06e36d67ffc70857a6f208", size = 1915733, upload-time = "2024-05-08T18:36:45.4Z" }, -] - -[[package]] -name = "unstructured-client" -version = "0.42.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiofiles" }, - { name = "cryptography" }, - { name = "httpcore" }, - { name = "httpx" }, - { name = "pydantic" }, - { name = "pypdf" }, - { name = "requests-toolbelt" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/fe/c6d334d4fb9a4a006125a1a8a3918be643c268290707d48e9cd060b71f7f/unstructured_client-0.42.6.tar.gz", hash = "sha256:ea54f2c4ca3e7a1330f9e77cbc96f88f829518beeec5e1b797b5352f4d76a73a", size = 94179, upload-time = "2025-12-17T03:49:58.38Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/12/5aa5d051b32d0c09077a8e83920e794b9bf2315739add4ab821e71fbca58/unstructured_client-0.42.6-py3-none-any.whl", hash = "sha256:c93b1d9d1b9f63a8e961729d00224b3659ef9ef3e14996ea4e53ddc95df671a9", size = 219563, upload-time = "2025-12-17T03:49:56.993Z" }, -] - -[[package]] -name = "urllib3" -version = "2.6.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930, upload-time = "2025-12-11T15:56:40.252Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182, upload-time = "2025-12-11T15:56:38.584Z" }, -] - -[[package]] -name = "uvicorn" -version = "0.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" }, -] - -[[package]] -name = "virtualenv" -version = "20.35.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "distlib" }, - { name = "filelock" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/20/28/e6f1a6f655d620846bd9df527390ecc26b3805a0c5989048c210e22c5ca9/virtualenv-20.35.4.tar.gz", hash = "sha256:643d3914d73d3eeb0c552cbb12d7e82adf0e504dbf86a3182f8771a153a1971c", size = 6028799, upload-time = "2025-10-29T06:57:40.511Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/0c/c05523fa3181fdf0c9c52a6ba91a23fbf3246cc095f26f6516f9c60e6771/virtualenv-20.35.4-py3-none-any.whl", hash = "sha256:c21c9cede36c9753eeade68ba7d523529f228a403463376cf821eaae2b650f1b", size = 6005095, upload-time = "2025-10-29T06:57:37.598Z" }, -] - -[[package]] -name = "wcwidth" -version = "0.2.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" }, -] - -[[package]] -name = "webencodings" -version = "0.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, -] - -[[package]] -name = "websockets" -version = "15.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" }, - { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" }, - { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" }, - { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" }, - { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" }, - { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" }, - { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" }, - { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" }, - { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" }, - { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" }, - { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, - { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, - { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, - { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, - { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, - { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, - { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, - { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, - { 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/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, -] - -[[package]] -name = "werkzeug" -version = "3.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" }, -] - -[[package]] -name = "wrapt" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/60/553997acf3939079dab022e37b67b1904b5b0cc235503226898ba573b10c/wrapt-2.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0e17283f533a0d24d6e5429a7d11f250a58d28b4ae5186f8f47853e3e70d2590", size = 77480, upload-time = "2025-11-07T00:43:30.573Z" }, - { url = "https://files.pythonhosted.org/packages/2d/50/e5b3d30895d77c52105c6d5cbf94d5b38e2a3dd4a53d22d246670da98f7c/wrapt-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85df8d92158cb8f3965aecc27cf821461bb5f40b450b03facc5d9f0d4d6ddec6", size = 60690, upload-time = "2025-11-07T00:43:31.594Z" }, - { url = "https://files.pythonhosted.org/packages/f0/40/660b2898703e5cbbb43db10cdefcc294274458c3ca4c68637c2b99371507/wrapt-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1be685ac7700c966b8610ccc63c3187a72e33cab53526a27b2a285a662cd4f7", size = 61578, upload-time = "2025-11-07T00:43:32.918Z" }, - { url = "https://files.pythonhosted.org/packages/5b/36/825b44c8a10556957bc0c1d84c7b29a40e05fcf1873b6c40aa9dbe0bd972/wrapt-2.0.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:df0b6d3b95932809c5b3fecc18fda0f1e07452d05e2662a0b35548985f256e28", size = 114115, upload-time = "2025-11-07T00:43:35.605Z" }, - { url = "https://files.pythonhosted.org/packages/83/73/0a5d14bb1599677304d3c613a55457d34c344e9b60eda8a737c2ead7619e/wrapt-2.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4da7384b0e5d4cae05c97cd6f94faaf78cc8b0f791fc63af43436d98c4ab37bb", size = 116157, upload-time = "2025-11-07T00:43:37.058Z" }, - { url = "https://files.pythonhosted.org/packages/01/22/1c158fe763dbf0a119f985d945711d288994fe5514c0646ebe0eb18b016d/wrapt-2.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ec65a78fbd9d6f083a15d7613b2800d5663dbb6bb96003899c834beaa68b242c", size = 112535, upload-time = "2025-11-07T00:43:34.138Z" }, - { url = "https://files.pythonhosted.org/packages/5c/28/4f16861af67d6de4eae9927799b559c20ebdd4fe432e89ea7fe6fcd9d709/wrapt-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7de3cc939be0e1174969f943f3b44e0d79b6f9a82198133a5b7fc6cc92882f16", size = 115404, upload-time = "2025-11-07T00:43:39.214Z" }, - { url = "https://files.pythonhosted.org/packages/a0/8b/7960122e625fad908f189b59c4aae2d50916eb4098b0fb2819c5a177414f/wrapt-2.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fb1a5b72cbd751813adc02ef01ada0b0d05d3dcbc32976ce189a1279d80ad4a2", size = 111802, upload-time = "2025-11-07T00:43:40.476Z" }, - { url = "https://files.pythonhosted.org/packages/3e/73/7881eee5ac31132a713ab19a22c9e5f1f7365c8b1df50abba5d45b781312/wrapt-2.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3fa272ca34332581e00bf7773e993d4f632594eb2d1b0b162a9038df0fd971dd", size = 113837, upload-time = "2025-11-07T00:43:42.921Z" }, - { url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" }, - { url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" }, - { url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" }, - { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" }, - { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" }, - { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" }, - { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" }, - { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" }, - { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" }, - { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" }, - { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" }, - { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" }, -] - -[[package]] -name = "yarg" -version = "0.1.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d4/c8/cc640404a0981e6c14e2044fc64e43b4c1ddf69e7dddc8f2a02638ba5ae8/yarg-0.1.9.tar.gz", hash = "sha256:55695bf4d1e3e7f756496c36a69ba32c40d18f821e38f61d028f6049e5e15911", size = 11988, upload-time = "2014-08-11T22:01:37.243Z" } -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.13.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/32/c7/c835debf13302f8aaf8d0561ac6ff5a9bc15cc140cd692a1330fb1900c55/zep_cloud-3.13.0.tar.gz", hash = "sha256:c55d9c511773bb2177ae8e08546141404f87d2099affafabd7ec4b4505763e48", size = 63116, upload-time = "2025-11-20T15:25:40.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/e1/bbf03c6c8007c0cb238780e7fc6d8e1a52633893933a41aa09678618985a/zep_cloud-3.13.0-py3-none-any.whl", hash = "sha256:b2fbdeef73e262194c8f67b58f76471de6ee87e1a629541a09d8f7bbf475f12b", size = 110601, upload-time = "2025-11-20T15:25:38.484Z" }, -] diff --git a/backend/vendor/camel-oasis/LICENSE b/backend/vendor/camel-oasis/LICENSE new file mode 100644 index 00000000..c46213dc --- /dev/null +++ b/backend/vendor/camel-oasis/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2023 @ CAMEL-AI.org + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/backend/vendor/camel-oasis/LICENSES/license_template.txt b/backend/vendor/camel-oasis/LICENSES/license_template.txt new file mode 100644 index 00000000..d963f8cc --- /dev/null +++ b/backend/vendor/camel-oasis/LICENSES/license_template.txt @@ -0,0 +1,13 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== diff --git a/backend/vendor/camel-oasis/LICENSES/update_license.py b/backend/vendor/camel-oasis/LICENSES/update_license.py new file mode 100644 index 00000000..882a9ff7 --- /dev/null +++ b/backend/vendor/camel-oasis/LICENSES/update_license.py @@ -0,0 +1,126 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import os +import re +import sys +from pathlib import Path +from typing import List + + +# The license template file is hard-coded with specific start and end lines +def fine_license_start_line(lines: List[str], start_with: str) -> int: + for i in range(len(lines)): + if lines[i].startswith(start_with): + return i + return None + + +def find_license_end_line(lines: List[str], start_with: str) -> int: + for i in range(len(lines) - 1, -1, -1): + if lines[i].startswith(start_with): + return i + return None + + +def update_license_in_file( + file_path: str, + license_template_path: str, + start_line_start_with: str, + end_line_start_with: str, +) -> bool: + with open(file_path, 'r', + encoding='utf-8') as f: # for windows compatibility + content = f.read() + + with open(license_template_path, 'r', encoding='utf-8') as f: + new_license = f.read().strip() + + maybe_existing_licenses = re.findall(r'^#.*?(?=\n)', content, + re.MULTILINE | re.DOTALL) + start_index = fine_license_start_line(maybe_existing_licenses, + start_line_start_with) + end_index = find_license_end_line(maybe_existing_licenses, + end_line_start_with) + if start_index is not None and end_index is not None: + maybe_existing_licenses = maybe_existing_licenses[ + start_index:end_index + 1] + else: + maybe_existing_licenses = None + if maybe_existing_licenses: + maybe_old_licenses = '\n'.join(maybe_existing_licenses) + if maybe_old_licenses.strip() != new_license.strip(): + replaced_content = content.replace(maybe_old_licenses, new_license) + with open(file_path, 'w') as f: + f.write(replaced_content) + print(f'Replaced license in {file_path}') + return True + else: + return False + else: + with open(file_path, 'w') as f: + f.write(new_license + '\n' + content) + print(f'Added license to {file_path}') + return True + + +def update_license_in_directory( + directory_path: str, + license_template_path: str, + start_line_start_with: str, + end_line_start_with: str, +) -> None: + # Check if directory exists + if not os.path.isdir(directory_path): + raise NotADirectoryError(f'{directory_path} is not a directory') + # Check if license template exists + if not os.path.isfile(license_template_path): + raise FileNotFoundError(f'{license_template_path} not found') + + file_count = 0 + for py_files in Path(directory_path).rglob("*.py"): + if py_files.name.startswith('.'): + continue + if any(part.startswith('.') for part in py_files.parts): + continue + if update_license_in_file( + py_files, + license_template_path, + start_line_start_with, + end_line_start_with, + ): + file_count += 1 + + print(f'License updated in {file_count} files') + + +if __name__ == '__main__': + if len(sys.argv) < 3: + print( + "Usage from command line: " + "python update_license.py " + "No valid input arguments found, please enter manually.") + directory_path = input("Enter directory path: ") + license_template_path = input("Enter license template path: ") + else: + directory_path = sys.argv[1] + license_template_path = sys.argv[2] + + start_line_start_with = "# =========== Copyright" + end_line_start_with = "# =========== Copyright" + update_license_in_directory( + directory_path, + license_template_path, + start_line_start_with, + end_line_start_with, + ) diff --git a/backend/vendor/camel-oasis/README.md b/backend/vendor/camel-oasis/README.md new file mode 100644 index 00000000..8987fbed --- /dev/null +++ b/backend/vendor/camel-oasis/README.md @@ -0,0 +1,337 @@ +
+ + banner + +
+ +
+ +
+ +

OASIS: Open Agent Social Interaction Simulations with One Million Agents +

+ +[![Documentation][docs-image]][docs-url] +[![Discord][discord-image]][discord-url] +[![X][x-image]][x-url] +[![Reddit][reddit-image]][reddit-url] +[![Wechat][wechat-image]][wechat-url] +[![Wechat][oasis-image]][oasis-url] +[![Hugging Face][huggingface-image]][huggingface-url] +[![Star][star-image]][star-url] +[![Package License][package-license-image]][package-license-url] + +

+ +[Community](https://github.com/camel-ai/camel#community) | +[Paper](https://arxiv.org/abs/2411.11581) | +[Examples](https://github.com/camel-ai/oasis/tree/main/scripts) | +[Dataset](https://huggingface.co/datasets/echo-yiyiyi/oasis-dataset) | +[Citation](https://github.com/camel-ai/oasis#-citation) | +[Contributing](https://github.com/camel-ai/oasis#-contributing-to-oasis) | +[CAMEL-AI](https://www.camel-ai.org/) + +

+ +
+ +
+ +

+ + +🏝️ OASIS is a scalable, open-source social media simulator that incorporates large language model agents to realistically mimic the behavior of up to one million users on platforms like Twitter and Reddit. It's designed to facilitate the study of complex social phenomena such as information spread, group polarization, and herd behavior, offering a versatile tool for exploring diverse social dynamics and user interactions in digital environments. + +

+ +
+ +
+🌟 Star OASIS on GitHub and be instantly notified of new releases. +
+ +
+ +
+ Star + +
+ +
+ +## ✨ Key Features + +### 📈 Scalability + +OASIS supports simulations of up to ***one million agents***, enabling studies of social media dynamics at a scale comparable to real-world platforms. + +### 📲 Dynamic Environments + +Adapts to real-time changes in social networks and content, mirroring the fluid dynamics of platforms like **Twitter** and **Reddit** for authentic simulation experiences. + +### 👍🏼 Diverse Action Spaces + +Agents can perform **23 actions**, such as following, commenting, and reposting, allowing for rich, multi-faceted interactions. + +### 🔥 Integrated Recommendation Systems + +Features **interest-based** and **hot-score-based recommendation algorithms**, simulating how users discover content and interact within social media platforms. + +
+ +## 📺 Demo Video + +### Introducing OASIS: Open Agent Social Interaction Simulations with One Million Agents + +https://github.com/user-attachments/assets/3bd2553c-d25d-4d8c-a739-1af51354b15a + +
+ +For more showcaes: + +- Can 1,000,000 AI agents simulate social media? + [→Watch demo](https://www.youtube.com/watch?v=lprGHqkApus&t=2s) + +
+ +## 🎯 Usecase + +
+ usecase1 + usecase2 + + usecase3 + + usecase4 +
+ +## ⚙️ Quick Start + +1. **Install the OASIS package:** + +Installing OASIS is a breeze thanks to its availability on PyPI. Simply open your terminal and run: + +```bash +pip install camel-oasis +``` + +2. **Set up your OpenAI API key:** + +```bash +# For Bash shell (Linux, macOS, Git Bash on Windows): +export OPENAI_API_KEY= + +# For Windows Command Prompt: +set OPENAI_API_KEY= +``` + +3. **Prepare the agent profile file:** + +Create the profile you want to assign to the agent. As an example, you can download [user_data_36.json](https://github.com/camel-ai/oasis/blob/main/data/reddit/user_data_36.json) and place it in your local `./data/reddit` folder. + +4. **Run the following Python code:** + +```python +import asyncio +import os + +from camel.models import ModelFactory +from camel.types import ModelPlatformType, ModelType + +import oasis +from oasis import (ActionType, LLMAction, ManualAction, + generate_reddit_agent_graph) + + +async def main(): + # Define the model for the agents + openai_model = ModelFactory.create( + model_platform=ModelPlatformType.OPENAI, + model_type=ModelType.GPT_4O_MINI, + ) + + # Define the available actions for the agents + available_actions = [ + ActionType.LIKE_POST, + ActionType.DISLIKE_POST, + ActionType.CREATE_POST, + ActionType.CREATE_COMMENT, + ActionType.LIKE_COMMENT, + ActionType.DISLIKE_COMMENT, + ActionType.SEARCH_POSTS, + ActionType.SEARCH_USER, + ActionType.TREND, + ActionType.REFRESH, + ActionType.DO_NOTHING, + ActionType.FOLLOW, + ActionType.MUTE, + ] + + agent_graph = await generate_reddit_agent_graph( + profile_path="./data/reddit/user_data_36.json", + model=openai_model, + available_actions=available_actions, + ) + + # Define the path to the database + db_path = "./data/reddit_simulation.db" + + # Delete the old database + if os.path.exists(db_path): + os.remove(db_path) + + # Make the environment + env = oasis.make( + agent_graph=agent_graph, + platform=oasis.DefaultPlatformType.REDDIT, + database_path=db_path, + ) + + # Run the environment + await env.reset() + + actions_1 = {} + actions_1[env.agent_graph.get_agent(0)] = [ + ManualAction(action_type=ActionType.CREATE_POST, + action_args={"content": "Hello, world!"}), + ManualAction(action_type=ActionType.CREATE_COMMENT, + action_args={ + "post_id": "1", + "content": "Welcome to the OASIS World!" + }) + ] + actions_1[env.agent_graph.get_agent(1)] = ManualAction( + action_type=ActionType.CREATE_COMMENT, + action_args={ + "post_id": "1", + "content": "I like the OASIS world." + }) + await env.step(actions_1) + + actions_2 = { + agent: LLMAction() + for _, agent in env.agent_graph.get_agents() + } + + # Perform the actions + await env.step(actions_2) + + # Close the environment + await env.close() + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +
+ +> \[!TIP\] +> For more detailed instructions and additional configuration options, check out the [documentation](https://docs.oasis.camel-ai.org/). + +### More Tutorials + +To discover how to create profiles for large-scale users, as well as how to visualize and analyze social simulation data once your experiment concludes, please refer to [More Tutorials](examples/experiment/user_generation_visualization.md) for detailed guidance. + +
+ Tutorial Overview +
+ +## 📢 News + +### Upcoming Features & Contributions + +> We welcome community contributions! Join us in building these exciting features. + +- [Support Multi Modal Platform](https://github.com/camel-ai/oasis/issues/47) + + + +### Latest Updates + +📢 Update the camel-ai version to 0.2.78 and update the dataset HuggingFace link. - 📆 December 4, 2025 + +- Add the report post action to mark inappropriate content. - 📆 June 8, 2025 +- Add features for creating group chats, sending messages in group chats, and leaving group chats. - 📆 June 6, 2025 +- Support Interview Action for asking agents specific questions and getting answers. - 📆 June 2, 2025 +- Support customization of each agent's models, tools, and prompts; refactor the interface to follow the PettingZoo style. - 📆 May 22, 2025 +- Refactor into the OASIS environment, publish camel-oasis on PyPI, and release the documentation. - 📆 April 24, 2025 +- Support OPENAI Embedding model for Twhin-Bert Recommendation System. - 📆 March 25, 2025 + ... +- Slightly refactoring the database to add Quote Action and modify Repost Action - 📆 January 13, 2025 +- Added the demo video and oasis's star history in the README - 📆 January 5, 2025 +- Introduced an Electronic Mall on the Reddit platform - 📆 December 5, 2024 +- OASIS initially released on arXiv - 📆 November 19, 2024 +- OASIS GitHub repository initially launched - 📆 November 19, 2024 + +## 🔎 Follow-up Research + +- [MultiAgent4Collusion](https://github.com/renqibing/MultiAgent4Collusion): multi-agent collusion simulation framework in social systems +- More to come... + +If your research is based on OASIS, we'd be happy to feature your work here—feel free to reach out or submit a pull request to add it to the [README](https://github.com/camel-ai/oasis/blob/main/README.md)! + +## 🥂 Contributing to OASIS🏝️ + +> We greatly appreciate your interest in contributing to our open-source initiative. To ensure a smooth collaboration and the success of contributions, we adhere to a set of contributing guidelines similar to those established by CAMEL. For a comprehensive understanding of the steps involved in contributing to our project, please refer to the OASIS [contributing guidelines](https://github.com/camel-ai/oasis/blob/master/CONTRIBUTING.md). 🤝🚀 +> +> An essential part of contributing involves not only submitting new features with accompanying tests (and, ideally, examples) but also ensuring that these contributions pass our automated pytest suite. This approach helps us maintain the project's quality and reliability by verifying compatibility and functionality. + +## 📬 Community & Contact + +If you're keen on exploring new research opportunities or discoveries with our platform and wish to dive deeper or suggest new features, we're here to talk. Feel free to get in touch for more details at camel.ai.team@gmail.com. + +
+ +- Join us ([*Discord*](https://discord.camel-ai.org/) or [*WeChat*](https://ghli.org/camel/wechat.png)) in pushing the boundaries of finding the scaling laws of agents. + +- Join WechatGroup for further discussions! + +
+ WeChat Group QR Code +
+ +## 🌟 Star History + +[![Star History Chart](https://api.star-history.com/svg?repos=camel-ai/oasis&type=Date)](https://star-history.com/#camel-ai/oasis&Date) + +## 🔗 Citation + +``` +@misc{yang2024oasisopenagentsocial, + title={OASIS: Open Agent Social Interaction Simulations with One Million Agents}, + author={Ziyi Yang and Zaibin Zhang and Zirui Zheng and Yuxian Jiang and Ziyue Gan and Zhiyu Wang and Zijian Ling and Jinsong Chen and Martz Ma and Bowen Dong and Prateek Gupta and Shuyue Hu and Zhenfei Yin and Guohao Li and Xu Jia and Lijun Wang and Bernard Ghanem and Huchuan Lu and Chaochao Lu and Wanli Ouyang and Yu Qiao and Philip Torr and Jing Shao}, + year={2024}, + eprint={2411.11581}, + archivePrefix={arXiv}, + primaryClass={cs.CL}, + url={https://arxiv.org/abs/2411.11581}, +} +``` + +## 🙌 Acknowledgment + +We would like to thank Douglas for designing the logo of our project. + +## 🖺 License + +The source code is licensed under Apache 2.0. + +[discord-image]: https://img.shields.io/discord/1082486657678311454?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb +[discord-url]: https://discord.camel-ai.org/ +[docs-image]: https://img.shields.io/badge/Documentation-EB3ECC +[docs-url]: https://docs.oasis.camel-ai.org/ +[huggingface-image]: https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-CAMEL--AI-ffc107?color=ffc107&logoColor=white +[huggingface-url]: https://huggingface.co/camel-ai +[oasis-image]: https://img.shields.io/badge/WeChat-OASISProject-brightgreen?logo=wechat&logoColor=white +[oasis-url]: ./assets/wechatgroup.png +[package-license-image]: https://img.shields.io/badge/License-Apache_2.0-blue.svg +[package-license-url]: https://github.com/camel-ai/oasis/blob/main/licenses/LICENSE +[reddit-image]: https://img.shields.io/reddit/subreddit-subscribers/CamelAI?style=plastic&logo=reddit&label=r%2FCAMEL&labelColor=white +[reddit-url]: https://www.reddit.com/r/CamelAI/ +[star-image]: https://img.shields.io/github/stars/camel-ai/oasis?label=stars&logo=github&color=brightgreen +[star-url]: https://github.com/camel-ai/oasis/stargazers +[wechat-image]: https://img.shields.io/badge/WeChat-CamelAIOrg-brightgreen?logo=wechat&logoColor=white +[wechat-url]: ./assets/wechat.JPGwechat.jpg +[x-image]: https://img.shields.io/twitter/follow/CamelAIOrg?style=social +[x-url]: https://x.com/CamelAIOrg diff --git a/backend/vendor/camel-oasis/oasis/__init__.py b/backend/vendor/camel-oasis/oasis/__init__.py new file mode 100644 index 00000000..ede59569 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/__init__.py @@ -0,0 +1,31 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +__version__ = "0.2.5" + +from oasis.environment.env_action import LLMAction, ManualAction +from oasis.environment.make import make +from oasis.social_agent import (generate_reddit_agent_graph, + generate_twitter_agent_graph) +from oasis.social_agent.agent import SocialAgent +from oasis.social_agent.agent_graph import AgentGraph +from oasis.social_platform.config import UserInfo +from oasis.social_platform.platform import Platform +from oasis.social_platform.typing import ActionType, DefaultPlatformType +from oasis.testing.show_db import print_db_contents + +__all__ = [ + "make", "Platform", "ActionType", "DefaultPlatformType", "ManualAction", + "LLMAction", "print_db_contents", "AgentGraph", "SocialAgent", "UserInfo", + "generate_reddit_agent_graph", "generate_twitter_agent_graph" +] diff --git a/backend/vendor/camel-oasis/oasis/clock/__init__.py b/backend/vendor/camel-oasis/oasis/clock/__init__.py new file mode 100644 index 00000000..3446142f --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/clock/__init__.py @@ -0,0 +1,16 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from .clock import Clock + +__all__ = ["Clock"] diff --git a/backend/vendor/camel-oasis/oasis/clock/clock.py b/backend/vendor/camel-oasis/oasis/clock/clock.py new file mode 100644 index 00000000..b0f8e197 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/clock/clock.py @@ -0,0 +1,33 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from datetime import datetime + + +class Clock: + r"""Clock used for the sandbox.""" + + def __init__(self, k: int = 1): + self.real_start_time = datetime.now() + self.k = k + self.time_step = 0 + + def time_transfer(self, now_time: datetime, + start_time: datetime) -> datetime: + time_diff = now_time - self.real_start_time + adjusted_diff = self.k * time_diff + adjusted_time = start_time + adjusted_diff + return adjusted_time + + def get_time_step(self) -> str: + return str(self.time_step) diff --git a/backend/vendor/camel-oasis/oasis/environment/__init__.py b/backend/vendor/camel-oasis/oasis/environment/__init__.py new file mode 100644 index 00000000..d963f8cc --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/environment/__init__.py @@ -0,0 +1,13 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== diff --git a/backend/vendor/camel-oasis/oasis/environment/env.py b/backend/vendor/camel-oasis/oasis/environment/env.py new file mode 100644 index 00000000..4d815c60 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/environment/env.py @@ -0,0 +1,208 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import asyncio +import logging +import os +from datetime import datetime +from typing import List, Union + +from oasis.environment.env_action import LLMAction, ManualAction +from oasis.social_agent.agent import SocialAgent +from oasis.social_agent.agent_graph import AgentGraph +from oasis.social_agent.agents_generator import generate_custom_agents +from oasis.social_platform.channel import Channel +from oasis.social_platform.platform import Platform +from oasis.social_platform.typing import (ActionType, DefaultPlatformType, + RecsysType) + +# Create log directory if it doesn't exist +log_dir = "./log" +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +# Configure logger +env_log = logging.getLogger("oasis.env") +env_log.setLevel("INFO") + +# Add file handler to save logs to file +current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") +file_handler = logging.FileHandler(f"{log_dir}/oasis-{current_time}.log", + encoding="utf-8") +file_handler.setLevel("INFO") +file_handler.setFormatter( + logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) +env_log.addHandler(file_handler) + + +class OasisEnv: + + def __init__( + self, + agent_graph: AgentGraph, + platform: Union[DefaultPlatformType, Platform], + database_path: str = None, + semaphore: int = 128, + ) -> None: + r"""Init the oasis environment. + + Args: + agent_graph: The AgentGraph to use in the simulation. + platform: The platform type to use. Including + `DefaultPlatformType.TWITTER` or `DefaultPlatformType.REDDIT`. + Or you can pass a custom `Platform` instance. + database_path: The path to create a sqlite3 database. The file + extension must be `.db` such as `twitter_simulation.db`. + """ + # Initialize the agent graph + self.agent_graph = agent_graph + # Use a semaphore to limit the number of concurrent requests + self.llm_semaphore = asyncio.Semaphore(semaphore) + if isinstance(platform, DefaultPlatformType): + if database_path is None: + raise ValueError( + "database_path is required for DefaultPlatformType") + self.platform = platform + if platform == DefaultPlatformType.TWITTER: + self.channel = Channel() + self.platform = Platform( + db_path=database_path, + channel=self.channel, + recsys_type="twhin-bert", + refresh_rec_post_count=2, + max_rec_post_len=2, + following_post_count=3, + ) + self.platform_type = DefaultPlatformType.TWITTER + elif platform == DefaultPlatformType.REDDIT: + self.channel = Channel() + self.platform = Platform( + db_path=database_path, + channel=self.channel, + recsys_type="reddit", + allow_self_rating=True, + show_score=True, + max_rec_post_len=100, + refresh_rec_post_count=5, + ) + self.platform_type = DefaultPlatformType.REDDIT + else: + raise ValueError(f"Invalid platform: {platform}. Only " + "DefaultPlatformType.TWITTER or " + "DefaultPlatformType.REDDIT are supported.") + elif isinstance(platform, Platform): + if database_path != platform.db_path: + env_log.warning("database_path is not the same as the " + "platform.db_path, using the platform.db_path") + self.platform = platform + self.channel = platform.channel + if platform.recsys_type == RecsysType.REDDIT: + self.platform_type = DefaultPlatformType.REDDIT + else: + self.platform_type = DefaultPlatformType.TWITTER + else: + raise ValueError( + f"Invalid platform: {platform}. You should pass a " + "DefaultPlatformType or a Platform instance.") + + async def reset(self) -> None: + r"""Start the platform and sign up the agents.""" + self.platform_task = asyncio.create_task(self.platform.running()) + self.agent_graph = await generate_custom_agents( + channel=self.channel, agent_graph=self.agent_graph) + + async def _perform_llm_action(self, agent): + r"""Send the request to the llm model and execute the action. + """ + async with self.llm_semaphore: + return await agent.perform_action_by_llm() + + async def _perform_interview_action(self, agent, interview_prompt: str): + r"""Send the request to the llm model and execute the interview. + """ + async with self.llm_semaphore: + return await agent.perform_interview(interview_prompt) + + async def step( + self, actions: dict[SocialAgent, Union[ManualAction, LLMAction, + List[Union[ManualAction, + LLMAction]]]] + ) -> None: + r"""Update the recommendation system and perform the actions. + + Args: + actions(dict[SocialAgent, Union[ManualAction, LLMAction, + List[Union[ManualAction, LLMAction]]]]): The actions to + perform, including the manual(pre-defined) actions and llm + actions. + Returns: + None + """ + # Update the recommendation system + await self.platform.update_rec_table() + env_log.info("update rec table.") + + # Create tasks for both manual and LLM actions + tasks = [] + for agent, action in actions.items(): + if isinstance(action, list): + for single_action in action: + if isinstance(single_action, ManualAction): + if single_action.action_type == ActionType.INTERVIEW: + # Use the agent's perform_interview method for + # interview actions + interview_prompt = single_action.action_args.get( + "prompt", "") + tasks.append( + self._perform_interview_action( + agent, interview_prompt)) + else: + tasks.append( + agent.perform_action_by_data( + single_action.action_type, + **single_action.action_args)) + elif isinstance(single_action, LLMAction): + tasks.append(self._perform_llm_action(agent)) + else: + if isinstance(action, ManualAction): + if action.action_type == ActionType.INTERVIEW: + # Use the agent's perform_interview method for + # interview actions + interview_prompt = action.action_args.get("prompt", "") + tasks.append( + self._perform_interview_action( + agent, interview_prompt)) + else: + tasks.append( + agent.perform_action_by_data( + action.action_type, **action.action_args)) + elif isinstance(action, LLMAction): + tasks.append(self._perform_llm_action(agent)) + + # Execute all tasks concurrently + await asyncio.gather(*tasks) + env_log.info("performed all actions.") + # # Control some agents to perform actions + # Update the clock + if self.platform_type == DefaultPlatformType.TWITTER: + self.platform.sandbox_clock.time_step += 1 + + async def close(self) -> None: + r"""Stop the platform and close the environment. + """ + await self.channel.write_to_receive_queue( + (None, None, ActionType.EXIT)) + await self.platform_task + env_log.info("Simulation finished! Please check the results in the " + f"database: {self.platform.db_path}. Note that the trace " + "table stored all the actions of the agents.") diff --git a/backend/vendor/camel-oasis/oasis/environment/env_action.py b/backend/vendor/camel-oasis/oasis/environment/env_action.py new file mode 100644 index 00000000..d2e44d2c --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/environment/env_action.py @@ -0,0 +1,45 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from dataclasses import dataclass +from typing import Any, Dict + +from oasis.social_platform.typing import ActionType + + +@dataclass +class ManualAction: + r"""Some manual predefined social platform actions that need to be + executed by certain agents. + + Args: + agent_id: The ID of the agent that will perform the action. + action: The action to perform. + args: The arguments to pass to the action. For details of each args in + each action, please refer to + `https://github.com/camel-ai/oasis/blob/main/oasis/social_agent/agent_action.py`. + """ + action_type: ActionType + action_args: Dict[str, Any] + + def init(self, action_type, action_args): + self.action_type = action_type + self.action_args = action_args + + +@dataclass +class LLMAction: + r"""Represents actions generated by a Language Learning Model (LLM).""" + + def init(self): + pass diff --git a/backend/vendor/camel-oasis/oasis/environment/make.py b/backend/vendor/camel-oasis/oasis/environment/make.py new file mode 100644 index 00000000..5d790b87 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/environment/make.py @@ -0,0 +1,19 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from oasis.environment.env import OasisEnv + + +def make(*args, **kwargs): + obj = OasisEnv(*args, **kwargs) + return obj diff --git a/backend/vendor/camel-oasis/oasis/social_agent/__init__.py b/backend/vendor/camel-oasis/oasis/social_agent/__init__.py new file mode 100644 index 00000000..003f03a2 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_agent/__init__.py @@ -0,0 +1,23 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from .agent import SocialAgent +from .agent_graph import AgentGraph +from .agents_generator import (generate_agents_100w, + generate_reddit_agent_graph, + generate_twitter_agent_graph) + +__all__ = [ + "SocialAgent", "AgentGraph", "generate_agents_100w", + "generate_reddit_agent_graph", "generate_twitter_agent_graph" +] diff --git a/backend/vendor/camel-oasis/oasis/social_agent/agent.py b/backend/vendor/camel-oasis/oasis/social_agent/agent.py new file mode 100644 index 00000000..5747a5df --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_agent/agent.py @@ -0,0 +1,321 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from __future__ import annotations + +import inspect +import logging +import sys +from datetime import datetime +from typing import TYPE_CHECKING, Any, Callable, List, Optional, Union + +from camel.agents import ChatAgent +from camel.messages import BaseMessage +from camel.models import BaseModelBackend, ModelManager +from camel.prompts import TextPrompt +from camel.toolkits import FunctionTool +from camel.types import OpenAIBackendRole + +from oasis.social_agent.agent_action import SocialAction +from oasis.social_agent.agent_environment import SocialEnvironment +from oasis.social_platform import Channel +from oasis.social_platform.config import UserInfo +from oasis.social_platform.typing import ActionType + +if TYPE_CHECKING: + from oasis.social_agent import AgentGraph + +if "sphinx" not in sys.modules: + agent_log = logging.getLogger(name="social.agent") + agent_log.setLevel("DEBUG") + + if not agent_log.handlers: + now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + file_handler = logging.FileHandler( + f"./log/social.agent-{str(now)}.log") + file_handler.setLevel("DEBUG") + file_handler.setFormatter( + logging.Formatter( + "%(levelname)s - %(asctime)s - %(name)s - %(message)s")) + agent_log.addHandler(file_handler) + +ALL_SOCIAL_ACTIONS = [action.value for action in ActionType] + + +class SocialAgent(ChatAgent): + r"""Social Agent.""" + + def __init__(self, + agent_id: int, + user_info: UserInfo, + user_info_template: TextPrompt | None = None, + channel: Channel | None = None, + model: Optional[Union[BaseModelBackend, + List[BaseModelBackend], + ModelManager]] = None, + agent_graph: "AgentGraph" = None, + available_actions: list[ActionType] = None, + tools: Optional[List[Union[FunctionTool, Callable]]] = None, + max_iteration: int = 1, + interview_record: bool = False): + self.social_agent_id = agent_id + self.user_info = user_info + self.channel = channel or Channel() + self.env = SocialEnvironment(SocialAction(agent_id, self.channel)) + if user_info_template is None: + system_message_content = self.user_info.to_system_message() + else: + system_message_content = self.user_info.to_custom_system_message( + user_info_template) + system_message = BaseMessage.make_assistant_message( + role_name="system", + content=system_message_content, # system prompt + ) + + if not available_actions: + agent_log.info("No available actions defined, using all actions.") + self.action_tools = self.env.action.get_openai_function_list() + else: + all_tools = self.env.action.get_openai_function_list() + all_possible_actions = [tool.func.__name__ for tool in all_tools] + + for action in available_actions: + action_name = action.value if isinstance( + action, ActionType) else action + if action_name not in all_possible_actions: + agent_log.warning( + f"Action {action_name} is not supported. Supported " + f"actions are: {', '.join(all_possible_actions)}") + self.action_tools = [ + tool for tool in all_tools if tool.func.__name__ in [ + a.value if isinstance(a, ActionType) else a + for a in available_actions + ] + ] + all_tools = (tools or []) + (self.action_tools or []) + super().__init__( + system_message=system_message, + model=model, + scheduling_strategy='random_model', + tools=all_tools, + ) + self.max_iteration = max_iteration + self.interview_record = interview_record + self.agent_graph = agent_graph + self.test_prompt = ( + "\n" + "Helen is a successful writer who usually writes popular western " + "novels. Now, she has an idea for a new novel that could really " + "make a big impact. If it works out, it could greatly " + "improve her career. But if it fails, she will have spent " + "a lot of time and effort for nothing.\n" + "\n" + "What do you think Helen should do?") + + async def perform_action_by_llm(self): + # Get posts: + env_prompt = await self.env.to_text_prompt() + user_msg = BaseMessage.make_user_message( + role_name="User", + content=( + f"Please perform social media actions after observing the " + f"platform environments. Notice that don't limit your " + f"actions for example to just like the posts. " + f"Here is your social media environment: {env_prompt}")) + try: + agent_log.info( + f"Agent {self.social_agent_id} observing environment: " + f"{env_prompt}") + response = await self.astep(user_msg) + for tool_call in response.info['tool_calls']: + action_name = tool_call.tool_name + args = tool_call.args + agent_log.info(f"Agent {self.social_agent_id} performed " + f"action: {action_name} with args: {args}") + if action_name not in ALL_SOCIAL_ACTIONS: + agent_log.info( + f"Agent {self.social_agent_id} get the result: " + f"{tool_call.result}") + # Abort graph action for if 100w Agent + # self.perform_agent_graph_action(action_name, args) + + return response + except Exception as e: + agent_log.error(f"Agent {self.social_agent_id} error: {e}") + return e + + async def perform_test(self): + """ + doing group polarization test for all agents. + TODO: rewrite the function according to the ChatAgent. + TODO: unify the test and interview function. + """ + # user conduct test to agent + _ = BaseMessage.make_user_message(role_name="User", + content=("You are a twitter user.")) + # Test memory should not be writed to memory. + # self.memory.write_record(MemoryRecord(user_msg, + # OpenAIBackendRole.USER)) + + openai_messages, num_tokens = self.memory.get_context() + + openai_messages = ([{ + "role": + self.system_message.role_name, + "content": + self.system_message.content.split("# RESPONSE FORMAT")[0], + }] + openai_messages + [{ + "role": "user", + "content": self.test_prompt + }]) + + agent_log.info(f"Agent {self.social_agent_id}: {openai_messages}") + # NOTE: this is a temporary solution. + # Camel can not stop updating the agents' memory after stop and astep + # now. + response = await self._aget_model_response( + openai_messages=openai_messages, num_tokens=num_tokens) + content = response.output_messages[0].content + agent_log.info( + f"Agent {self.social_agent_id} receive response: {content}") + return { + "user_id": self.social_agent_id, + "prompt": openai_messages, + "content": content + } + + async def perform_interview(self, interview_prompt: str): + """ + Perform an interview with the agent. + """ + # user conduct test to agent + user_msg = BaseMessage.make_user_message( + role_name="User", content=("You are a twitter user.")) + + if self.interview_record: + # Test memory should not be writed to memory. + self.update_memory(message=user_msg, role=OpenAIBackendRole.SYSTEM) + + openai_messages, num_tokens = self.memory.get_context() + + openai_messages = ([{ + "role": + self.system_message.role_name, + "content": + self.system_message.content.split("# RESPONSE FORMAT")[0], + }] + openai_messages + [{ + "role": "user", + "content": interview_prompt + }]) + + agent_log.info(f"Agent {self.social_agent_id}: {openai_messages}") + # NOTE: this is a temporary solution. + # Camel can not stop updating the agents' memory after stop and astep + # now. + + response = await self._aget_model_response( + openai_messages=openai_messages, num_tokens=num_tokens) + + content = response.output_messages[0].content + + if self.interview_record: + # Test memory should not be writed to memory. + self.update_memory(message=response.output_messages[0], + role=OpenAIBackendRole.USER) + agent_log.info( + f"Agent {self.social_agent_id} receive response: {content}") + + # Record the complete interview (prompt + response) through the channel + interview_data = {"prompt": interview_prompt, "response": content} + result = await self.env.action.perform_action( + interview_data, ActionType.INTERVIEW.value) + + # Return the combined result + return { + "user_id": self.social_agent_id, + "prompt": openai_messages, + "content": content, + "success": result.get("success", False) + } + + async def perform_action_by_hci(self) -> Any: + print("Please choose one function to perform:") + function_list = self.env.action.get_openai_function_list() + for i in range(len(function_list)): + agent_log.info(f"Agent {self.social_agent_id} function: " + f"{function_list[i].func.__name__}") + + selection = int(input("Enter your choice: ")) + if not 0 <= selection < len(function_list): + agent_log.error(f"Agent {self.social_agent_id} invalid input.") + return + func = function_list[selection].func + + params = inspect.signature(func).parameters + args = [] + for param in params.values(): + while True: + try: + value = input(f"Enter value for {param.name}: ") + args.append(value) + break + except ValueError: + agent_log.error("Invalid input, please enter an integer.") + + result = await func(*args) + return result + + async def perform_action_by_data(self, func_name, *args, **kwargs) -> Any: + func_name = func_name.value if isinstance(func_name, + ActionType) else func_name + function_list = self.env.action.get_openai_function_list() + for i in range(len(function_list)): + if function_list[i].func.__name__ == func_name: + func = function_list[i].func + result = await func(*args, **kwargs) + self.update_memory(message=BaseMessage.make_user_message( + role_name=OpenAIBackendRole.SYSTEM, + content=f"Agent {self.social_agent_id} performed " + f"{func_name} with args: {args} and kwargs: {kwargs}" + f"and the result is {result}"), + role=OpenAIBackendRole.SYSTEM) + agent_log.info(f"Agent {self.social_agent_id}: {result}") + return result + raise ValueError(f"Function {func_name} not found in the list.") + + def perform_agent_graph_action( + self, + action_name: str, + arguments: dict[str, Any], + ): + r"""Remove edge if action is unfollow or add edge + if action is follow to the agent graph. + """ + if "unfollow" in action_name: + followee_id: int | None = arguments.get("followee_id", None) + if followee_id is None: + return + self.agent_graph.remove_edge(self.social_agent_id, followee_id) + agent_log.info( + f"Agent {self.social_agent_id} unfollowed Agent {followee_id}") + elif "follow" in action_name: + followee_id: int | None = arguments.get("followee_id", None) + if followee_id is None: + return + self.agent_graph.add_edge(self.social_agent_id, followee_id) + agent_log.info( + f"Agent {self.social_agent_id} followed Agent {followee_id}") + + def __str__(self) -> str: + return (f"{self.__class__.__name__}(agent_id={self.social_agent_id}, " + f"model_type={self.model_type.value})") diff --git a/backend/vendor/camel-oasis/oasis/social_agent/agent_action.py b/backend/vendor/camel-oasis/oasis/social_agent/agent_action.py new file mode 100644 index 00000000..078cdbb4 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_agent/agent_action.py @@ -0,0 +1,758 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from typing import Any + +from camel.toolkits import FunctionTool + +from oasis.social_platform.channel import Channel +from oasis.social_platform.typing import ActionType + + +class SocialAction: + + def __init__(self, agent_id: int, channel: Channel): + self.agent_id = agent_id + self.channel = channel + + def get_openai_function_list(self) -> list[FunctionTool]: + return [ + FunctionTool(func) for func in [ + self.create_post, + self.like_post, + self.repost, + self.quote_post, + self.unlike_post, + self.dislike_post, + self.undo_dislike_post, + self.search_posts, + self.search_user, + self.trend, + self.refresh, + self.do_nothing, + self.create_comment, + self.like_comment, + self.dislike_comment, + self.unlike_comment, + self.undo_dislike_comment, + self.follow, + self.unfollow, + self.mute, + self.unmute, + self.purchase_product, + self.interview, + self.report_post, + self.join_group, + self.leave_group, + self.send_to_group, + self.create_group, + self.listen_from_group, + ] + ] + + async def perform_action(self, message: Any, type: str): + message_id = await self.channel.write_to_receive_queue( + (self.agent_id, message, type)) + response = await self.channel.read_from_send_queue(message_id) + return response[2] + + async def sign_up(self, user_name: str, name: str, bio: str): + r"""Signs up a new user with the provided username, name, and bio. + + This method prepares a user message comprising the user's details and + invokes an asynchronous action to perform the sign-up process. On + successful execution, it returns a dictionary indicating success along + with the newly created user ID. + + Args: + user_name (str): The username for the new user. + name (str): The full name of the new user. + bio (str): A brief biography of the new user. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the sign-up was + successful, and 'user_id' key maps to the integer ID of the + newly created user on success. + + Example of a successful return: + {'success': True, 'user_id': 2} + """ + + # print(f"Agent {self.agent_id} is signing up with " + # f"user_name: {user_name}, name: {name}, bio: {bio}") + user_message = (user_name, name, bio) + return await self.perform_action(user_message, ActionType.SIGNUP.value) + + async def refresh(self): + r"""Refresh to get recommended posts. + + This method invokes an asynchronous action to refresh and fetch + recommended posts. On successful execution, it returns a dictionary + indicating success along with a list of posts. Each post in the list + contains details such as post ID, user ID, content, creation date, + and the number of likes. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the refresh is + successful. The 'posts' key maps to a list of dictionaries, + each representing a post with its details. + + Example of a successful return: + { + "success": True, + "posts": [ + { + "post_id": 1, + "user_id": 23, + "content": "This is an example post content.", + "created_at": "2024-05-14T12:00:00Z", + "num_likes": 5 + }, + { + "post_id": 2, + "user_id": 42, + "content": "Another example post content.", + "created_at": "2024-05-14T12:05:00Z", + "num_likes": 15 + } + ] + } + """ + return await self.perform_action(None, ActionType.REFRESH.value) + + async def do_nothing(self): + """Perform no action. + Returns: + dict: A dictionary with 'success' indicating if the removal was + successful. + Example of a successful return: + {"success": True} + """ + return await self.perform_action(None, ActionType.DO_NOTHING.value) + + async def create_post(self, content: str): + r"""Create a new post with the given content. + + This method invokes an asynchronous action to create a new post based + on the provided content. Upon successful execution, it returns a + dictionary indicating success and the ID of the newly created post. + + Args: + content (str): The content of the post to be created. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the post creation was + successful. The 'post_id' key maps to the integer ID of the + newly created post. + + Example of a successful return: + {'success': True, 'post_id': 50} + """ + return await self.perform_action(content, ActionType.CREATE_POST.value) + + async def repost(self, post_id: int): + r"""Repost a specified post. + + This method invokes an asynchronous action to Repost a specified + post. It is identified by the given post ID. Upon successful + execution, it returns a dictionary indicating success and the ID of + the newly created repost. + + Args: + post_id (int): The ID of the post to be reposted. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the Repost creation was + successful. The 'post_id' key maps to the integer ID of the + newly created repost. + + Example of a successful return: + {"success": True, "post_id": 123} + + Note: + Attempting to repost a post that the user has already reposted + will result in a failure. + """ + return await self.perform_action(post_id, ActionType.REPOST.value) + + async def quote_post(self, post_id: int, quote_content: str): + r"""Quote a specified post with a given quote content. + + This method invokes an asynchronous action to quote a specified post + with a given quote content. Upon successful execution, it returns a + dictionary indicating success and the ID of the newly created quote. + + Args: + post_id (int): The ID of the post to be quoted. + quote_content (str): The content of the quote to be created. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the quote creation was + successful. The 'post_id' key maps to the integer ID of the + newly created quote. + + Example of a successful return: + {"success": True, "post_id": 123} + + Note: + Attempting to quote a post that the user has already quoted will + result in a failure. + """ + quote_message = (post_id, quote_content) + return await self.perform_action(quote_message, ActionType.QUOTE_POST) + + async def like_post(self, post_id: int): + r"""Create a new like for a specified post. + + This method invokes an asynchronous action to create a new like for a + post. It is identified by the given post ID. Upon successful + execution, it returns a dictionary indicating success and the ID of + the newly created like. + + Args: + post_id (int): The ID of the post to be liked. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the like creation was + successful. The 'like_id' key maps to the integer ID of the + newly created like. + + Example of a successful return: + {"success": True, "like_id": 123} + + Note: + Attempting to like a post that the user has already liked will + result in a failure. + """ + return await self.perform_action(post_id, ActionType.LIKE_POST.value) + + async def unlike_post(self, post_id: int): + """Remove a like for a post. + + This method removes a like from the database, identified by the + post's ID. It returns a dictionary indicating the success of the + operation and the ID of the removed like. + + Args: + post_id (int): The ID of the post to be unliked. + + Returns: + dict: A dictionary with 'success' indicating if the removal was + successful, and 'like_id' the ID of the removed like. + + Example of a successful return: + {"success": True, "like_id": 123} + + Note: + Attempting to remove a like for a post that the user has not + previously liked will result in a failure. + """ + return await self.perform_action(post_id, ActionType.UNLIKE_POST.value) + + async def dislike_post(self, post_id: int): + r"""Create a new dislike for a specified post. + + This method invokes an asynchronous action to create a new dislike for + a post. It is identified by the given post ID. Upon successful + execution, it returns a dictionary indicating success and the ID of + the newly created dislike. + + Args: + post_id (int): The ID of the post to be disliked. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the dislike creation was + successful. The 'dislike_id' key maps to the integer ID of the + newly created like. + + Example of a successful return: + {"success": True, "dislike_id": 123} + + Note: + Attempting to dislike a post that the user has already liked will + result in a failure. + """ + return await self.perform_action(post_id, + ActionType.DISLIKE_POST.value) + + async def undo_dislike_post(self, post_id: int): + """Remove a dislike for a post. + + This method removes a dislike from the database, identified by the + post's ID. It returns a dictionary indicating the success of the + operation and the ID of the removed dislike. + + Args: + post_id (int): The ID of the post to be unliked. + + Returns: + dict: A dictionary with 'success' indicating if the removal was + successful, and 'dislike_id' the ID of the removed like. + + Example of a successful return: + {"success": True, "dislike_id": 123} + + Note: + Attempting to remove a dislike for a post that the user has not + previously liked will result in a failure. + """ + return await self.perform_action(post_id, + ActionType.UNDO_DISLIKE_POST.value) + + async def search_posts(self, query: str): + r"""Search posts based on a given query. + + This method performs a search operation in the database for posts + that match the given query string. The search considers the + post's content, post ID, and user ID. It returns a dictionary + indicating the operation's success and, if successful, a list of + posts that match the query. + + Args: + query (str): The search query string. The search is performed + against the post's content, post ID, and user ID. + + Returns: + dict: A dictionary with a 'success' key indicating the operation's + success. On success, it includes a 'posts' key with a list of + dictionaries, each representing a post. On failure, it + includes an 'error' message or a 'message' indicating no + posts were found. + + Example of a successful return: + { + "success": True, + "posts": [ + { + "post_id": 1, + "user_id": 42, + "content": "Hello, world!", + "created_at": "2024-05-14T12:00:00Z", + "num_likes": 150 + }, + ... + ] + } + """ + return await self.perform_action(query, ActionType.SEARCH_POSTS.value) + + async def search_user(self, query: str): + r"""Search users based on a given query. + + This asynchronous method performs a search operation in the database + for users that match the given query string. The search considers the + user's username, name, bio, and user ID. It returns a dictionary + indicating the operation's success and, if successful, a list of users + that match the query. + + Args: + query (str): The search query string. The search is performed + against the user's username, name, bio, and user ID. + + Returns: + dict: A dictionary with a 'success' key indicating the operation's + success. On success, it includes a 'users' key with a list of + dictionaries, each representing a user. On failure, it includes + an 'error' message or a 'message' indicating no users were + found. + + Example of a successful return: + { + "success": True, + "users": [ + { + "user_id": 1, + "user_name": "exampleUser", + "name": "John Doe", + "bio": "This is an example bio", + "created_at": "2024-05-14T12:00:00Z", + "num_followings": 100, + "num_followers": 150 + }, + ... + ] + } + """ + return await self.perform_action(query, ActionType.SEARCH_USER.value) + + async def follow(self, followee_id: int): + r"""Follow a user. + + This method allows agent to follow another user (followee). + It checks if the agent initiating the follow request has a + corresponding user ID and if the follow relationship already exists. + + Args: + followee_id (int): The user ID of the user to be followed. + + Returns: + dict: A dictionary with a 'success' key indicating the operation's + success. On success, it includes a 'follow_id' key with the ID + of the newly created follow record. On failure, it includes an + 'error' message. + + Example of a successful return: + {"success": True, "follow_id": 123} + """ + return await self.perform_action(followee_id, ActionType.FOLLOW.value) + + async def unfollow(self, followee_id: int): + r"""Unfollow a user. + + This method allows agent to unfollow another user (followee). It + checks if the agent initiating the unfollow request has a + corresponding user ID and if the follow relationship exists. If so, + it removes the follow record from the database, updates the followers + and followings count for both users, and logs the action. + + Args: + followee_id (int): The user ID of the user to be unfollowed. + + Returns: + dict: A dictionary with a 'success' key indicating the operation's + success. On success, it includes a 'follow_id' key with the ID + of the removed follow record. On failure, it includes an + 'error' message. + + Example of a successful return: + {"success": True, "follow_id": 123} + """ + return await self.perform_action(followee_id, + ActionType.UNFOLLOW.value) + + async def mute(self, mutee_id: int): + r"""Mute a user. + + Allows agent to mute another user. Checks for an existing mute + record before adding a new one to the database. + + Args: + mutee_id (int): ID of the user to be muted. + + Returns: + dict: On success, returns a dictionary with 'success': True and + mute_id' of the new record. On failure, returns 'success': + False and an 'error' message. + + Example of a successful return: + {"success": True, "mutee_id": 123} + """ + return await self.perform_action(mutee_id, ActionType.MUTE.value) + + async def unmute(self, mutee_id: int): + r"""Unmute a user. + + Allows agent to remove a mute on another user. Checks for an + existing mute record before removing it from the database. + + Args: + mutee_id (int): ID of the user to be unmuted. + + Returns: + dict: On success, returns a dictionary with 'success': True and + 'mutee_id' of the unmuted record. On failure, returns + 'success': False and an 'error' message. + + Example of a successful return: + {"success": True, "mutee_id": 123} + """ + return await self.perform_action(mutee_id, ActionType.UNMUTE.value) + + async def trend(self): + r"""Fetch the trending posts within a predefined time period. + + Retrieves the top K posts with the most likes in the last specified + number of days. + + Returns: + dict: On success, returns a dictionary with 'success': True and a + list of 'posts', each post being a dictionary containing + 'post_id', 'user_id', 'content', 'created_at', and + 'num_likes'. On failure, returns 'success': False and an + 'error' message or a message indicating no trending posts + were found. + + Example of a successful return: + { + "success": True, + "posts": [ + { + "post_id": 123, + "user_id": 456, + "content": "Example post content", + "created_at": "2024-05-14T12:00:00", + "num_likes": 789 + }, + ... + ] + } + """ + return await self.perform_action(None, ActionType.TREND.value) + + async def create_comment(self, post_id: int, content: str): + r"""Create a new comment for a specified post given content. + + This method creates a new comment based on the provided content and + associates it with the given post ID. Upon successful execution, it + returns a dictionary indicating success and the ID of the newly created + comment. + + Args: + post_id (int): The ID of the post to which the comment is to be + added. + content (str): The content of the comment to be created. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the comment creation was + successful. The 'comment_id' key maps to the integer ID of the + newly created comment. + + Example of a successful return: + {'success': True, 'comment_id': 123} + """ + comment_message = (post_id, content) + return await self.perform_action(comment_message, + ActionType.CREATE_COMMENT.value) + + async def like_comment(self, comment_id: int): + r"""Create a new like for a specified comment. + + This method invokes an action to create a new like for a comment, + identified by the given comment ID. Upon successful execution, it + returns a dictionary indicating success and the ID of the newly + created like. + + Args: + comment_id (int): The ID of the comment to be liked. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the like creation was + successful. The 'like_id' key maps to the integer ID of the + newly created like. + + Example of a successful return: + {"success": True, "comment_like_id": 456} + + Note: + Attempting to like a comment that the user has already liked will + result in a failure. + """ + return await self.perform_action(comment_id, + ActionType.LIKE_COMMENT.value) + + async def unlike_comment(self, comment_id: int): + """Remove a like for a comment based on the comment's ID. + + This method removes a like from the database, identified by the + comment's ID. It returns a dictionary indicating the success of the + operation and the ID of the removed like. + + Args: + comment_id (int): The ID of the comment to be unliked. + + Returns: + dict: A dictionary with 'success' indicating if the removal was + successful, and 'like_id' the ID of the removed like. + + Example of a successful return: + {"success": True, "like_id": 456} + + Note: + Attempting to remove a like for a comment that the user has not + previously liked will result in a failure. + """ + return await self.perform_action(comment_id, + ActionType.UNLIKE_COMMENT.value) + + async def dislike_comment(self, comment_id: int): + r"""Create a new dislike for a specified comment. + + This method invokes an action to create a new dislike for a + comment, identified by the given comment ID. Upon successful execution, + it returns a dictionary indicating success and the ID of the newly + created dislike. + + Args: + comment_id (int): The ID of the comment to be disliked. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the dislike creation was + successful. The 'dislike_id' key maps to the integer ID of the + newly created dislike. + + Example of a successful return: + {"success": True, "comment_dislike_id": 456} + + Note: + Attempting to dislike a comment that the user has already liked + will result in a failure. + """ + return await self.perform_action(comment_id, + ActionType.DISLIKE_COMMENT.value) + + async def undo_dislike_comment(self, comment_id: int): + """Remove a dislike for a comment. + + This method removes a dislike from the database, identified by the + comment's ID. It returns a dictionary indicating the success of the + operation and the ID of the removed dislike. + + Args: + comment_id (int): The ID of the comment to have the dislike + removed. + + Returns: + dict: A dictionary with 'success' indicating if the removal was + successful, and 'dislike_id' the ID of the removed dislike. + + Example of a successful return: + {"success": True, "dislike_id": 456} + + Note: + Attempting to remove a dislike for a comment that the user has not + previously disliked will result in a failure. + """ + return await self.perform_action(comment_id, + ActionType.UNDO_DISLIKE_COMMENT.value) + + async def purchase_product(self, product_name: str, purchase_num: int): + r"""Purchase a product. + + Args: + product_name (str): The name of the product to be purchased. + purchase_num (int): The number of products to be purchased. + + Returns: + dict: A dictionary with 'success' indicating if the purchase was + successful. + """ + purchase_message = (product_name, purchase_num) + return await self.perform_action(purchase_message, + ActionType.PURCHASE_PRODUCT.value) + + async def interview(self, prompt: str): + r"""Interview an agent with the given prompt. + + This method invokes an asynchronous action to interview an agent with a + specific prompt question. Upon successful execution, + it returns a dictionary containing a success status + and an interview_id for tracking. + + Args: + prompt (str): The interview question or prompt to ask the agent. + + Returns: + dict: A dictionary containing success status and an interview_id. + + Example of a successful return: + { + "success": True, + "interview_id": "1621234567_0" # Timestamp_UserID format + } + """ + return await self.perform_action(prompt, ActionType.INTERVIEW.value) + + async def report_post(self, post_id: int, report_reason: str): + r"""Report a specified post with a given reason. + + This method invokes an asynchronous action to report a specified post + with a given reason. Upon successful execution, it returns a + dictionary indicating success and the ID of the newly created report. + + Args: + post_id (int): The ID of the post to be reported. + report_reason (str): The reason for reporting the post. + + Returns: + dict: A dictionary with two key-value pairs. The 'success' key + maps to a boolean indicating whether the report creation was + successful. The 'report_id' key maps to the integer ID of the + newly created report. + + Example of a successful return: + {"success": True, "report_id": 123} + + Note: + Attempting to report a post that the user has already reported will + result in a failure. + """ + report_message = (post_id, report_reason) + return await self.perform_action(report_message, + ActionType.REPORT_POST.value) + + async def create_group(self, group_name: str): + r"""Creates a new group on the platform. + + Args: + group_name (str): The name of the group to be created. + + Returns: + dict: Platform response indicating success or failure, + e.g.{"success": True, "group_id": 1} + """ + return await self.perform_action(group_name, + ActionType.CREATE_GROUP.value) + + async def join_group(self, group_id: int): + r"""Joins a group with the specified ID. + + Args: + group_id (int): The ID of the group to join. + + Returns: + dict: Platform response indicating success or failure, + e.g. {"success": True} + """ + return await self.perform_action(group_id, ActionType.JOIN_GROUP.value) + + async def leave_group(self, group_id: int): + r"""Leaves a group with the specified ID. + + Args: + group_id (int): The ID of the group to leave. + + Returns: + dict: Platform response indicating success or failure, e.g. + {"success": True} + """ + return await self.perform_action(group_id, + ActionType.LEAVE_GROUP.value) + + async def send_to_group(self, group_id: int, message: str): + r"""Sends a message to a specific group. + + Args: + group_id (int): The ID of the target group. + message (str): The content of the message to send. + + Returns: + dict: Platform response indicating success or failure, e.g. + {"success": True, "message_id": 123} + """ + return await self.perform_action((group_id, message), + ActionType.SEND_TO_GROUP.value) + + async def listen_from_group(self): + r"""Listen messages from groups""" + return await self.perform_action(self.agent_id, + ActionType.LISTEN_FROM_GROUP.value) diff --git a/backend/vendor/camel-oasis/oasis/social_agent/agent_environment.py b/backend/vendor/camel-oasis/oasis/social_agent/agent_environment.py new file mode 100644 index 00000000..d2051919 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_agent/agent_environment.py @@ -0,0 +1,135 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from __future__ import annotations + +import json +import sqlite3 +from abc import ABC, abstractmethod +from string import Template + +from oasis.social_agent.agent_action import SocialAction +from oasis.social_platform.database import get_db_path + + +class Environment(ABC): + + @abstractmethod + def to_text_prompt(self) -> str: + r"""Convert the environment to text prompt.""" + raise NotImplementedError + + +class SocialEnvironment(Environment): + followers_env_template = Template("I have $num_followers followers.") + follows_env_template = Template("I have $num_follows follows.") + + posts_env_template = Template( + "After refreshing, you see some posts $posts") + + groups_env_template = Template( + "And there are many group chat channels $all_groups\n" + "And You are already in some groups $joined_groups\n" + "You receive some messages from them $messages\n" + "You can join the groups you are interested, " + "leave the groups you already in, send messages to the group " + "you already in.\n" + "You must make sure you can only send messages to the group you " + "are already in") + env_template = Template( + "$groups_env\n" + "$posts_env\npick one you want to perform action that best " + "reflects your current inclination based on your profile and " + "posts content. Do not limit your action in just `like` to like posts") + + def __init__(self, action: SocialAction): + self.action = action + + async def get_posts_env(self) -> str: + posts = await self.action.refresh() + # TODO: Replace posts json format string to other formats + if posts["success"]: + posts_env = json.dumps(posts["posts"], indent=4) + posts_env = self.posts_env_template.substitute(posts=posts_env) + else: + posts_env = "After refreshing, there are no existing posts." + return posts_env + + async def get_followers_env(self) -> str: + # TODO: Implement followers env + agent_id = self.action.agent_id + db_path = get_db_path() + try: + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute("SELECT num_followers FROM user WHERE agent_id = ?", + (agent_id, )) + result = cursor.fetchone() + num_followers = result[0] if result else 0 + conn.close() + except Exception: + num_followers = 0 + return self.followers_env_template.substitute( + {"num_followers": num_followers}) + + async def get_follows_env(self) -> str: + # TODO: Implement follows env + agent_id = self.action.agent_id + try: + db_path = get_db_path() + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + cursor.execute( + "SELECT num_followings FROM user WHERE agent_id = ?", + (agent_id, )) + result = cursor.fetchone() + num_followings = result[0] if result else 0 + conn.close() + except Exception: + num_followings = 0 + return self.follows_env_template.substitute( + {"num_follows": num_followings}) + + async def get_group_env(self) -> str: + groups = await self.action.listen_from_group() + if groups["success"]: + all_groups = json.dumps(groups["all_groups"]) + joined_groups = json.dumps(groups["joined_groups"]) + messages = json.dumps(groups["messages"]) + groups_env = self.groups_env_template.substitute( + all_groups=all_groups, + joined_groups=joined_groups, + messages=messages, + ) + else: + groups_env = "No groups." + return groups_env + + async def to_text_prompt( + self, + include_posts: bool = True, + include_followers: bool = True, + include_follows: bool = True, + ) -> str: + followers_env = (await self.get_followers_env() + if include_follows else "No followers.") + follows_env = (await self.get_follows_env() + if include_followers else "No follows.") + posts_env = await self.get_posts_env() if include_posts else "" + + return self.env_template.substitute( + followers_env=followers_env, + follows_env=follows_env, + posts_env=posts_env, + groups_env=await self.get_group_env(), + ) diff --git a/backend/vendor/camel-oasis/oasis/social_agent/agent_graph.py b/backend/vendor/camel-oasis/oasis/social_agent/agent_graph.py new file mode 100644 index 00000000..c7819314 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_agent/agent_graph.py @@ -0,0 +1,292 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from __future__ import annotations + +from typing import Any, Literal + +import igraph as ig +from neo4j import GraphDatabase + +from oasis.social_agent.agent import SocialAgent +from oasis.social_platform.config import Neo4jConfig + + +class Neo4jHandler: + + def __init__(self, nei4j_config: Neo4jConfig): + self.driver = GraphDatabase.driver( + nei4j_config.uri, + auth=(nei4j_config.username, nei4j_config.password), + ) + self.driver.verify_connectivity() + + def close(self): + self.driver.close() + + def create_agent(self, agent_id: int): + with self.driver.session() as session: + session.write_transaction(self._create_and_return_agent, agent_id) + + def delete_agent(self, agent_id: int): + with self.driver.session() as session: + session.write_transaction( + self._delete_agent_and_relationships, + agent_id, + ) + + def get_number_of_nodes(self) -> int: + with self.driver.session() as session: + return session.read_transaction(self._get_number_of_nodes) + + def get_number_of_edges(self) -> int: + with self.driver.session() as session: + return session.read_transaction(self._get_number_of_edges) + + def add_edge(self, src_agent_id: int, dst_agent_id: int): + with self.driver.session() as session: + session.write_transaction( + self._add_and_return_edge, + src_agent_id, + dst_agent_id, + ) + + def remove_edge(self, src_agent_id: int, dst_agent_id: int): + with self.driver.session() as session: + session.write_transaction( + self._remove_and_return_edge, + src_agent_id, + dst_agent_id, + ) + + def get_all_nodes(self) -> list[int]: + with self.driver.session() as session: + return session.read_transaction(self._get_all_nodes) + + def get_all_edges(self) -> list[tuple[int, int]]: + with self.driver.session() as session: + return session.read_transaction(self._get_all_edges) + + def reset_graph(self): + with self.driver.session() as session: + session.write_transaction(self._reset_graph) + + @staticmethod + def _create_and_return_agent(tx: Any, agent_id: int): + query = """ + CREATE (a:Agent {id: $agent_id}) + RETURN a + """ + result = tx.run(query, agent_id=agent_id) + return result.single() + + @staticmethod + def _delete_agent_and_relationships(tx: Any, agent_id: int): + query = """ + MATCH (a:Agent {id: $agent_id}) + DETACH DELETE a + RETURN count(a) AS deleted + """ + result = tx.run(query, agent_id=agent_id) + return result.single() + + @staticmethod + def _add_and_return_edge(tx: Any, src_agent_id: int, dst_agent_id: int): + query = """ + MATCH (a:Agent {id: $src_agent_id}), (b:Agent {id: $dst_agent_id}) + CREATE (a)-[r:FOLLOW]->(b) + RETURN r + """ + result = tx.run(query, + src_agent_id=src_agent_id, + dst_agent_id=dst_agent_id) + return result.single() + + @staticmethod + def _remove_and_return_edge(tx: Any, src_agent_id: int, dst_agent_id: int): + query = """ + MATCH (a:Agent {id: $src_agent_id}) + MATCH (b:Agent {id: $dst_agent_id}) + MATCH (a)-[r:FOLLOW]->(b) + DELETE r + RETURN count(r) AS deleted + """ + result = tx.run(query, + src_agent_id=src_agent_id, + dst_agent_id=dst_agent_id) + return result.single() + + @staticmethod + def _get_number_of_nodes(tx: Any) -> int: + query = """ + MATCH (n) + RETURN count(n) AS num_nodes + """ + result = tx.run(query) + return result.single()["num_nodes"] + + @staticmethod + def _get_number_of_edges(tx: Any) -> int: + query = """ + MATCH ()-[r]->() + RETURN count(r) AS num_edges + """ + result = tx.run(query) + return result.single()["num_edges"] + + @staticmethod + def _get_all_nodes(tx: Any) -> list[int]: + query = """ + MATCH (a:Agent) + RETURN a.id AS agent_id + """ + result = tx.run(query) + return [record["agent_id"] for record in result] + + @staticmethod + def _get_all_edges(tx: Any) -> list[tuple[int, int]]: + query = """ + MATCH (a:Agent)-[r:FOLLOW]->(b:Agent) + RETURN a.id AS src_agent_id, b.id AS dst_agent_id + """ + result = tx.run(query) + return [(record["src_agent_id"], record["dst_agent_id"]) + for record in result] + + @staticmethod + def _reset_graph(tx: Any): + query = """ + MATCH (n) + DETACH DELETE n + """ + tx.run(query) + + +class AgentGraph: + r"""AgentGraph class to manage the social graph of agents.""" + + def __init__( + self, + backend: Literal["igraph", "neo4j"] = "igraph", + neo4j_config: Neo4jConfig | None = None, + ): + self.backend = backend + if self.backend == "igraph": + self.graph = ig.Graph(directed=True) + else: + assert neo4j_config is not None + assert neo4j_config.is_valid() + self.graph = Neo4jHandler(neo4j_config) + self.agent_mappings: dict[int, SocialAgent] = {} + + def reset(self): + if self.backend == "igraph": + self.graph = ig.Graph(directed=True) + else: + self.graph.reset_graph() + self.agent_mappings: dict[int, SocialAgent] = {} + + def add_agent(self, agent: SocialAgent): + if self.backend == "igraph": + self.graph.add_vertex(agent.social_agent_id) + else: + self.graph.create_agent(agent.social_agent_id) + self.agent_mappings[agent.social_agent_id] = agent + + def add_edge(self, agent_id_0: int, agent_id_1: int): + try: + self.graph.add_edge(agent_id_0, agent_id_1) + except Exception: + pass + + def remove_agent(self, agent: SocialAgent): + if self.backend == "igraph": + self.graph.delete_vertices(agent.social_agent_id) + else: + self.graph.delete_agent(agent.social_agent_id) + del self.agent_mappings[agent.social_agent_id] + + def remove_edge(self, agent_id_0: int, agent_id_1: int): + if self.backend == "igraph": + if self.graph.are_connected(agent_id_0, agent_id_1): + self.graph.delete_edges([(agent_id_0, agent_id_1)]) + else: + self.graph.remove_edge(agent_id_0, agent_id_1) + + def get_agent(self, agent_id: int) -> SocialAgent: + return self.agent_mappings[agent_id] + + def get_agents( + self, + agent_ids: list[int] = None) -> list[tuple[int, SocialAgent]]: + if agent_ids: + return [(agent_id, self.get_agent(agent_id)) + for agent_id in agent_ids] + if self.backend == "igraph": + return [(node.index, self.agent_mappings[node.index]) + for node in self.graph.vs] + else: + return [(agent_id, self.agent_mappings[agent_id]) + for agent_id in self.graph.get_all_nodes()] + + def get_edges(self) -> list[tuple[int, int]]: + if self.backend == "igraph": + return [(edge.source, edge.target) for edge in self.graph.es] + else: + return self.graph.get_all_edges() + + def get_num_nodes(self) -> int: + if self.backend == "igraph": + return self.graph.vcount() + else: + return self.graph.get_number_of_nodes() + + def get_num_edges(self) -> int: + if self.backend == "igraph": + return self.graph.ecount() + else: + return self.graph.get_number_of_edges() + + def close(self) -> None: + if self.backend == "neo4j": + self.graph.close() + + def visualize( + self, + path: str, + vertex_size: int = 20, + edge_arrow_size: float = 0.5, + with_labels: bool = True, + vertex_color: str = "#f74f1b", + vertex_frame_width: int = 2, + width: int = 1000, + height: int = 1000, + ): + if self.backend == "neo4j": + raise ValueError("Neo4j backend does not support visualization.") + layout = self.graph.layout("auto") + if with_labels: + labels = [node_id for node_id, _ in self.get_agents()] + else: + labels = None + ig.plot( + self.graph, + target=path, + layout=layout, + vertex_label=labels, + vertex_size=vertex_size, + vertex_color=vertex_color, + edge_arrow_size=edge_arrow_size, + vertex_frame_width=vertex_frame_width, + bbox=(width, height), + ) diff --git a/backend/vendor/camel-oasis/oasis/social_agent/agents_generator.py b/backend/vendor/camel-oasis/oasis/social_agent/agents_generator.py new file mode 100644 index 00000000..65435906 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_agent/agents_generator.py @@ -0,0 +1,649 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from __future__ import annotations + +import ast +import asyncio +import json +from typing import List, Optional, Union + +import pandas as pd +import tqdm +from camel.memories import MemoryRecord +from camel.messages import BaseMessage +from camel.models import BaseModelBackend, ModelManager +from camel.types import OpenAIBackendRole + +from oasis.social_agent import AgentGraph, SocialAgent +from oasis.social_platform import Channel, Platform +from oasis.social_platform.config import Neo4jConfig, UserInfo +from oasis.social_platform.typing import ActionType + + +async def generate_agents( + agent_info_path: str, + channel: Channel, + model: Union[BaseModelBackend, List[BaseModelBackend]], + start_time, + recsys_type: str = "twitter", + twitter: Platform = None, + available_actions: list[ActionType] = None, + neo4j_config: Neo4jConfig | None = None, +) -> AgentGraph: + """TODO: need update the description of args and check + Generate and return a dictionary of agents from the agent + information CSV file. Each agent is added to the database and + their respective profiles are updated. + + Args: + agent_info_path (str): The file path to the agent information CSV file. + channel (Channel): Information channel. + action_space_prompt (str): determine the action space of agents. + model_random_seed (int): Random seed to randomly assign model to + each agent. (default: 42) + cfgs (list, optional): List of configuration. (default: `None`) + neo4j_config (Neo4jConfig, optional): Neo4j graph database + configuration. (default: `None`) + + Returns: + dict: A dictionary of agent IDs mapped to their respective agent + class instances. + """ + agent_info = pd.read_csv(agent_info_path) + + agent_graph = (AgentGraph() if neo4j_config is None else AgentGraph( + backend="neo4j", + neo4j_config=neo4j_config, + )) + + # agent_graph = [] + sign_up_list = [] + follow_list = [] + user_update1 = [] + user_update2 = [] + post_list = [] + + for agent_id in range(len(agent_info)): + profile = { + "nodes": [], + "edges": [], + "other_info": {}, + } + profile["other_info"]["user_profile"] = agent_info["user_char"][ + agent_id] + + user_info = UserInfo( + name=agent_info["username"][agent_id], + description=agent_info["description"][agent_id], + profile=profile, + recsys_type=recsys_type, + ) + + agent = SocialAgent( + agent_id=agent_id, + user_info=user_info, + channel=channel, + model=model, + agent_graph=agent_graph, + available_actions=available_actions, + ) + + agent_graph.add_agent(agent) + # TODO we should not use following_count and followers_count + # We should calculate the number of followings and followers + # based on the graph because the following situation is dynamic. + num_followings = 0 + num_followers = 0 + + sign_up_list.append(( + agent_id, + agent_id, + agent_info["username"][agent_id], + agent_info["name"][agent_id], + agent_info["description"][agent_id], + start_time, + num_followings, + num_followers, + )) + + following_id_list = ast.literal_eval( + agent_info["following_agentid_list"][agent_id]) + if not isinstance(following_id_list, int): + if len(following_id_list) != 0: + for follow_id in following_id_list: + follow_list.append((agent_id, follow_id, start_time)) + user_update1.append((agent_id, )) + user_update2.append((follow_id, )) + agent_graph.add_edge(agent_id, follow_id) + + previous_posts = ast.literal_eval( + agent_info["previous_tweets"][agent_id]) + if len(previous_posts) != 0: + for post in previous_posts: + post_list.append((agent_id, post, start_time, 0, 0)) + + # generate_log.info('agent gegenerate finished.') + + user_insert_query = ( + "INSERT INTO user (user_id, agent_id, user_name, name, bio, " + "created_at, num_followings, num_followers) VALUES " + "(?, ?, ?, ?, ?, ?, ?, ?)") + twitter.pl_utils._execute_many_db_command(user_insert_query, + sign_up_list, + commit=True) + + follow_insert_query = ( + "INSERT INTO follow (follower_id, followee_id, created_at) " + "VALUES (?, ?, ?)") + twitter.pl_utils._execute_many_db_command(follow_insert_query, + follow_list, + commit=True) + user_update_query1 = ( + "UPDATE user SET num_followings = num_followings + 1 " + "WHERE user_id = ?") + twitter.pl_utils._execute_many_db_command(user_update_query1, + user_update1, + commit=True) + + user_update_query2 = ("UPDATE user SET num_followers = num_followers + 1 " + "WHERE user_id = ?") + twitter.pl_utils._execute_many_db_command(user_update_query2, + user_update2, + commit=True) + + # generate_log.info('twitter followee update finished.') + + post_insert_query = ( + "INSERT INTO post (user_id, content, created_at, num_likes, " + "num_dislikes) VALUES (?, ?, ?, ?, ?)") + twitter.pl_utils._execute_many_db_command(post_insert_query, + post_list, + commit=True) + + # generate_log.info('twitter creat post finished.') + + return agent_graph + + +async def generate_agents_100w( + agent_info_path: str, + channel: Channel, + start_time, + model: Union[BaseModelBackend, List[BaseModelBackend]], + recsys_type: str = "twitter", + twitter: Platform = None, + available_actions: list[ActionType] = None, +) -> List: + """ TODO: need update the description of args. + Generate and return a dictionary of agents from the agent + information CSV file. Each agent is added to the database and + their respective profiles are updated. + + Args: + agent_info_path (str): The file path to the agent information CSV file. + channel (Channel): Information channel. + action_space_prompt (str): determine the action space of agents. + model_random_seed (int): Random seed to randomly assign model to + each agent. (default: 42) + + Returns: + dict: A dictionary of agent IDs mapped to their respective agent + class instances. + """ + agent_info = pd.read_csv(agent_info_path) + + # TODO when setting 100w agents, the agentgraph class is too slow. + # I use the list. + agent_graph = [] + # agent_graph = (AgentGraph() if neo4j_config is None else AgentGraph( + # backend="neo4j", + # neo4j_config=neo4j_config, + # )) + + # agent_graph = [] + sign_up_list = [] + follow_list = [] + user_update1 = [] + user_update2 = [] + post_list = [] + + # precompute to speed up agent generation in one million scale + _ = agent_info["following_agentid_list"].apply(ast.literal_eval) + previous_tweets_lists = agent_info["previous_tweets"].apply( + ast.literal_eval) + previous_tweets_lists = agent_info['previous_tweets'].apply( + ast.literal_eval) + following_id_lists = agent_info["following_agentid_list"].apply( + ast.literal_eval) + + for agent_id in tqdm.tqdm(range(len(agent_info))): + profile = { + "nodes": [], + "edges": [], + "other_info": {}, + } + profile["other_info"]["user_profile"] = agent_info["user_char"][ + agent_id] + # TODO if you simulate one million agents, use active threshold below. + # profile['other_info']['active_threshold'] = [0.01] * 24 + + user_info = UserInfo( + name=agent_info["username"][agent_id], + description=agent_info["description"][agent_id], + profile=profile, + recsys_type=recsys_type, + ) + + agent = SocialAgent( + agent_id=agent_id, + user_info=user_info, + channel=channel, + model=model, + agent_graph=agent_graph, + available_actions=available_actions, + ) + + agent_graph.append(agent) + num_followings = 0 + num_followers = 0 + # print('agent_info["following_count"]', agent_info["following_count"]) + + # TODO some data does not cotain this key. + if 'following_count' not in agent_info.columns: + agent_info['following_count'] = 0 + if 'followers_count' not in agent_info.columns: + agent_info['followers_count'] = 0 + + if not agent_info["following_count"].empty: + num_followings = agent_info["following_count"][agent_id] + if not agent_info["followers_count"].empty: + num_followers = agent_info["followers_count"][agent_id] + + sign_up_list.append(( + agent_id, + agent_id, + agent_info["username"][agent_id], + agent_info["name"][agent_id], + agent_info["description"][agent_id], + start_time, + num_followings, + num_followers, + )) + + following_id_list = following_id_lists[agent_id] + + # TODO If we simulate 1 million agents, we can not use agent_graph + # class. It is not scalble. + if not isinstance(following_id_list, int): + if len(following_id_list) != 0: + for follow_id in following_id_list: + follow_list.append((agent_id, follow_id, start_time)) + user_update1.append((agent_id, )) + user_update2.append((follow_id, )) + # agent_graph.add_edge(agent_id, follow_id) + + previous_posts = previous_tweets_lists[agent_id] + if len(previous_posts) != 0: + for post in previous_posts: + post_list.append((agent_id, post, start_time, 0, 0)) + + # generate_log.info('agent gegenerate finished.') + + user_insert_query = ( + "INSERT INTO user (user_id, agent_id, user_name, name, bio, " + "created_at, num_followings, num_followers) VALUES " + "(?, ?, ?, ?, ?, ?, ?, ?)") + twitter.pl_utils._execute_many_db_command(user_insert_query, + sign_up_list, + commit=True) + + follow_insert_query = ( + "INSERT INTO follow (follower_id, followee_id, created_at) " + "VALUES (?, ?, ?)") + twitter.pl_utils._execute_many_db_command(follow_insert_query, + follow_list, + commit=True) + + if not (agent_info["following_count"].empty + and agent_info["followers_count"].empty): + user_update_query1 = ( + "UPDATE user SET num_followings = num_followings + 1 " + "WHERE user_id = ?") + twitter.pl_utils._execute_many_db_command(user_update_query1, + user_update1, + commit=True) + + user_update_query2 = ( + "UPDATE user SET num_followers = num_followers + 1 " + "WHERE user_id = ?") + twitter.pl_utils._execute_many_db_command(user_update_query2, + user_update2, + commit=True) + + # generate_log.info('twitter followee update finished.') + + post_insert_query = ( + "INSERT INTO post (user_id, content, created_at, num_likes, " + "num_dislikes) VALUES (?, ?, ?, ?, ?)") + twitter.pl_utils._execute_many_db_command(post_insert_query, + post_list, + commit=True) + + # generate_log.info('twitter creat post finished.') + + return agent_graph + + +async def generate_controllable_agents( + channel: Channel, + control_user_num: int, +) -> tuple[AgentGraph, dict]: + agent_graph = AgentGraph() + agent_user_id_mapping = {} + for i in range(control_user_num): + user_info = UserInfo( + is_controllable=True, + profile={"other_info": { + "user_profile": "None" + }}, + recsys_type="reddit", + ) + # controllable的agent_id全都在llm agent的agent_id的前面 + agent = SocialAgent(agent_id=i, + user_info=user_info, + channel=channel, + agent_graph=agent_graph) + # Add agent to the agent graph + agent_graph.add_agent(agent) + + username = input(f"Please input username for agent {i}: ") + name = input(f"Please input name for agent {i}: ") + bio = input(f"Please input bio for agent {i}: ") + + response = await agent.env.action.sign_up(username, name, bio) + user_id = response["user_id"] + agent_user_id_mapping[i] = user_id + + for i in range(control_user_num): + for j in range(control_user_num): + agent = agent_graph.get_agent(i) + # controllable agent互相也全部关注 + if i != j: + user_id = agent_user_id_mapping[j] + await agent.env.action.follow(user_id) + agent_graph.add_edge(i, j) + return agent_graph, agent_user_id_mapping + + +async def gen_control_agents_with_data( + channel: Channel, + control_user_num: int, + models: list[BaseModelBackend] | None = None, +) -> tuple[AgentGraph, dict]: + agent_graph = AgentGraph() + agent_user_id_mapping = {} + for i in range(control_user_num): + user_info = UserInfo( + is_controllable=True, + profile={ + "other_info": { + "user_profile": "None", + "gender": "None", + "mbti": "None", + "country": "None", + "age": "None", + } + }, + recsys_type="reddit", + ) + # controllable的agent_id全都在llm agent的agent_id的前面 + agent = SocialAgent( + agent_id=i, + user_info=user_info, + channel=channel, + agent_graph=agent_graph, + model=models, + available_actions=None, + ) + # Add agent to the agent graph + agent_graph.add_agent(agent) + user_name = "momo" + name = "momo" + bio = "None." + response = await agent.env.action.sign_up(user_name, name, bio) + user_id = response["user_id"] + agent_user_id_mapping[i] = user_id + + return agent_graph, agent_user_id_mapping + + +async def generate_reddit_agents( + agent_info_path: str, + channel: Channel, + agent_graph: AgentGraph | None = None, + agent_user_id_mapping: dict[int, int] | None = None, + follow_post_agent: bool = False, + mute_post_agent: bool = False, + model: Optional[Union[BaseModelBackend, List[BaseModelBackend], + ModelManager]] = None, + available_actions: list[ActionType] = None, +) -> AgentGraph: + if agent_user_id_mapping is None: + agent_user_id_mapping = {} + if agent_graph is None: + agent_graph = AgentGraph() + + control_user_num = agent_graph.get_num_nodes() + + with open(agent_info_path, "r") as file: + agent_info = json.load(file) + + async def process_agent(i): + # Instantiate an agent + profile = { + "nodes": [], # Relationships with other agents + "edges": [], # Relationship details + "other_info": {}, + } + # Update agent profile with additional information + profile["other_info"]["user_profile"] = agent_info[i]["persona"] + profile["other_info"]["mbti"] = agent_info[i]["mbti"] + profile["other_info"]["gender"] = agent_info[i]["gender"] + profile["other_info"]["age"] = agent_info[i]["age"] + profile["other_info"]["country"] = agent_info[i]["country"] + + user_info = UserInfo( + name=agent_info[i]["username"], + description=agent_info[i]["bio"], + profile=profile, + recsys_type="reddit", + ) + + agent = SocialAgent( + agent_id=i + control_user_num, + user_info=user_info, + channel=channel, + agent_graph=agent_graph, + model=model, + available_actions=available_actions, + ) + + # Add agent to the agent graph + agent_graph.add_agent(agent) + + # Sign up agent and add their information to the database + # print(f"Signing up agent {agent_info['username'][i]}...") + response = await agent.env.action.sign_up(agent_info[i]["username"], + agent_info[i]["realname"], + agent_info[i]["bio"]) + user_id = response["user_id"] + agent_user_id_mapping[i + control_user_num] = user_id + + if follow_post_agent: + await agent.env.action.follow(1) + content = """ +{ + "reason": "He is my friend, and I would like to follow him " + "on social media.", + "functions": [ + { + "name": "follow", + "arguments": { + "user_id": 1 + } + } + ] +} +""" + + agent_msg = BaseMessage.make_assistant_message( + role_name="Assistant", content=content) + agent.memory.write_record( + MemoryRecord(agent_msg, OpenAIBackendRole.ASSISTANT)) + elif mute_post_agent: + await agent.env.action.mute(1) + content = """ +{ + "reason": "He is my enemy, and I would like to mute him on social media.", + "functions": [{ + "name": "mute", + "arguments": { + "user_id": 1 + } +} +""" + agent_msg = BaseMessage.make_assistant_message( + role_name="Assistant", content=content) + agent.memory.write_record( + MemoryRecord(agent_msg, OpenAIBackendRole.ASSISTANT)) + + tasks = [process_agent(i) for i in range(len(agent_info))] + await asyncio.gather(*tasks) + + return agent_graph + + +def connect_platform_channel( + channel: Channel, + agent_graph: AgentGraph | None = None, +) -> AgentGraph: + for _, agent in agent_graph.get_agents(): + agent.channel = channel + agent.env.action.channel = channel + return agent_graph + + +async def generate_custom_agents( + channel: Channel, + agent_graph: AgentGraph | None = None, +) -> AgentGraph: + if agent_graph is None: + agent_graph = AgentGraph() + + agent_graph = connect_platform_channel(channel=channel, + agent_graph=agent_graph) + + sign_up_tasks = [ + agent.env.action.sign_up(user_name=agent.user_info.user_name, + name=agent.user_info.name, + bio=agent.user_info.description) + for _, agent in agent_graph.get_agents() + ] + await asyncio.gather(*sign_up_tasks) + return agent_graph + + +async def generate_reddit_agent_graph( + profile_path: str, + model: Optional[Union[BaseModelBackend, List[BaseModelBackend], + ModelManager]] = None, + available_actions: list[ActionType] = None, +) -> AgentGraph: + agent_graph = AgentGraph() + with open(profile_path, "r") as file: + agent_info = json.load(file) + + async def process_agent(i): + # Instantiate an agent + profile = { + "nodes": [], # Relationships with other agents + "edges": [], # Relationship details + "other_info": {}, + } + # Update agent profile with additional information + profile["other_info"]["user_profile"] = agent_info[i]["persona"] + profile["other_info"]["mbti"] = agent_info[i]["mbti"] + profile["other_info"]["gender"] = agent_info[i]["gender"] + profile["other_info"]["age"] = agent_info[i]["age"] + profile["other_info"]["country"] = agent_info[i]["country"] + + user_info = UserInfo( + name=agent_info[i]["username"], + description=agent_info[i]["bio"], + profile=profile, + recsys_type="reddit", + ) + + agent = SocialAgent( + agent_id=i, + user_info=user_info, + agent_graph=agent_graph, + model=model, + available_actions=available_actions, + ) + + # Add agent to the agent graph + agent_graph.add_agent(agent) + + tasks = [process_agent(i) for i in range(len(agent_info))] + await asyncio.gather(*tasks) + return agent_graph + + +async def generate_twitter_agent_graph( + profile_path: str, + model: Optional[Union[BaseModelBackend, List[BaseModelBackend], + ModelManager]] = None, + available_actions: list[ActionType] = None, +) -> AgentGraph: + agent_info = pd.read_csv(profile_path) + + agent_graph = AgentGraph() + + for agent_id in range(len(agent_info)): + profile = { + "nodes": [], + "edges": [], + "other_info": {}, + } + profile["other_info"]["user_profile"] = agent_info["user_char"][ + agent_id] + + user_info = UserInfo( + name=agent_info["username"][agent_id], + description=agent_info["description"][agent_id], + profile=profile, + recsys_type='twitter', + ) + + agent = SocialAgent( + agent_id=agent_id, + user_info=user_info, + model=model, + agent_graph=agent_graph, + available_actions=available_actions, + ) + + agent_graph.add_agent(agent) + return agent_graph diff --git a/backend/vendor/camel-oasis/oasis/social_platform/__init__.py b/backend/vendor/camel-oasis/oasis/social_platform/__init__.py new file mode 100644 index 00000000..83c0aca4 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/__init__.py @@ -0,0 +1,20 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from .channel import Channel +from .platform import Platform + +__all__ = [ + "Channel", + "Platform", +] diff --git a/backend/vendor/camel-oasis/oasis/social_platform/channel.py b/backend/vendor/camel-oasis/oasis/social_platform/channel.py new file mode 100644 index 00000000..7d29f053 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/channel.py @@ -0,0 +1,71 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import asyncio +import uuid + + +class AsyncSafeDict: + + def __init__(self): + self.dict = {} + self.lock = asyncio.Lock() + + async def put(self, key, value): + async with self.lock: + self.dict[key] = value + + async def get(self, key, default=None): + async with self.lock: + return self.dict.get(key, default) + + async def pop(self, key, default=None): + async with self.lock: + return self.dict.pop(key, default) + + async def keys(self): + async with self.lock: + return list(self.dict.keys()) + + +class Channel: + + def __init__(self): + self.receive_queue = asyncio.Queue() # Used to store received messages + # Using an asynchronous safe dictionary to store messages to be sent + self.send_dict = AsyncSafeDict() + + async def receive_from(self): + message = await self.receive_queue.get() + return message + + async def send_to(self, message): + # message_id is the first element of the message + message_id = message[0] + await self.send_dict.put(message_id, message) + + async def write_to_receive_queue(self, action_info): + message_id = str(uuid.uuid4()) + await self.receive_queue.put((message_id, action_info)) + return message_id + + async def read_from_send_queue(self, message_id): + while True: + if message_id in await self.send_dict.keys(): + # Attempting to retrieve the message + message = await self.send_dict.pop(message_id, None) + if message: + return message # Return the found message + # Temporarily suspend to avoid tight looping + await asyncio.sleep( + 0.1) # set a large one to reduce the workload of cpu diff --git a/backend/vendor/camel-oasis/oasis/social_platform/config/__init__.py b/backend/vendor/camel-oasis/oasis/social_platform/config/__init__.py new file mode 100644 index 00000000..8bfb9b4d --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/config/__init__.py @@ -0,0 +1,20 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from .neo4j import Neo4jConfig +from .user import UserInfo + +__all__ = [ + "UserInfo", + "Neo4jConfig", +] diff --git a/backend/vendor/camel-oasis/oasis/social_platform/config/neo4j.py b/backend/vendor/camel-oasis/oasis/social_platform/config/neo4j.py new file mode 100644 index 00000000..af5aacd5 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/config/neo4j.py @@ -0,0 +1,24 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from dataclasses import dataclass + + +@dataclass +class Neo4jConfig: + uri: str | None = None + username: str | None = None + password: str | None = None + + def is_valid(self) -> bool: + return all([self.uri, self.username, self.password]) diff --git a/backend/vendor/camel-oasis/oasis/social_platform/config/user.py b/backend/vendor/camel-oasis/oasis/social_platform/config/user.py new file mode 100644 index 00000000..a36b4253 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/config/user.py @@ -0,0 +1,111 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# flake8: noqa: E501 +import warnings +from dataclasses import dataclass +from typing import Any + +from camel.prompts import TextPrompt + + +@dataclass +class UserInfo: + user_name: str | None = None + name: str | None = None + description: str | None = None + profile: dict[str, Any] | None = None + recsys_type: str = "twitter" + is_controllable: bool = False + + def to_custom_system_message(self, user_info_template: TextPrompt) -> str: + required_keys = user_info_template.key_words + info_keys = set(self.profile.keys()) + missing = required_keys - info_keys + extra = info_keys - required_keys + if missing: + raise ValueError( + f"Missing required keys in UserInfo.profile: {missing}") + if extra: + warnings.warn(f"Extra keys not used in UserInfo.profile: {extra}") + + return user_info_template.format(**self.profile) + + def to_system_message(self) -> str: + if self.recsys_type != "reddit": + return self.to_twitter_system_message() + else: + return self.to_reddit_system_message() + + def to_twitter_system_message(self) -> str: + name_string = "" + description_string = "" + if self.name is not None: + name_string = f"Your name is {self.name}." + if self.profile is None: + description = name_string + elif "other_info" not in self.profile: + description = name_string + elif "user_profile" in self.profile["other_info"]: + if self.profile["other_info"]["user_profile"] is not None: + user_profile = self.profile["other_info"]["user_profile"] + description_string = f"Your have profile: {user_profile}." + description = f"{name_string}\n{description_string}" + + system_content = f""" +# OBJECTIVE +You're a Twitter user, and I'll present you with some posts. After you see the posts, choose some actions from the following functions. + +# SELF-DESCRIPTION +Your actions should be consistent with your self-description and personality. +{description} + +# RESPONSE METHOD +Please perform actions by tool calling. + """ + + return system_content + + def to_reddit_system_message(self) -> str: + name_string = "" + description_string = "" + if self.name is not None: + name_string = f"Your name is {self.name}." + if self.profile is None: + description = name_string + elif "other_info" not in self.profile: + description = name_string + elif "user_profile" in self.profile["other_info"]: + if self.profile["other_info"]["user_profile"] is not None: + user_profile = self.profile["other_info"]["user_profile"] + description_string = f"Your have profile: {user_profile}." + description = f"{name_string}\n{description_string}" + print(self.profile['other_info']) + description += ( + f"You are a {self.profile['other_info']['gender']}, " + f"{self.profile['other_info']['age']} years old, with an MBTI " + f"personality type of {self.profile['other_info']['mbti']} from " + f"{self.profile['other_info']['country']}.") + + system_content = f""" +# OBJECTIVE +You're a Reddit user, and I'll present you with some tweets. After you see the tweets, choose some actions from the following functions. + +# SELF-DESCRIPTION +Your actions should be consistent with your self-description and personality. +{description} + +# RESPONSE METHOD +Please perform actions by tool calling. +""" + return system_content diff --git a/backend/vendor/camel-oasis/oasis/social_platform/database.py b/backend/vendor/camel-oasis/oasis/social_platform/database.py new file mode 100644 index 00000000..4d8f83a8 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/database.py @@ -0,0 +1,291 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from __future__ import annotations + +import os +import os.path as osp +import sqlite3 +from typing import Any, Dict, List + +SCHEMA_DIR = "social_platform/schema" +DB_DIR = "data" +DB_NAME = "social_media.db" + +USER_SCHEMA_SQL = "user.sql" +POST_SCHEMA_SQL = "post.sql" +FOLLOW_SCHEMA_SQL = "follow.sql" +MUTE_SCHEMA_SQL = "mute.sql" +LIKE_SCHEMA_SQL = "like.sql" +DISLIKE_SCHEMA_SQL = "dislike.sql" +REPORT_SCHEAM_SQL = "report.sql" +TRACE_SCHEMA_SQL = "trace.sql" +REC_SCHEMA_SQL = "rec.sql" +COMMENT_SCHEMA_SQL = "comment.sql" +COMMENT_LIKE_SCHEMA_SQL = "comment_like.sql" +COMMENT_DISLIKE_SCHEMA_SQL = "comment_dislike.sql" +PRODUCT_SCHEMA_SQL = "product.sql" +GROUP_SCHEMA_SQL = "chat_group.sql" +GROUP_MEMBER_SCHEMA_SQL = "group_member.sql" +GROUP_MESSAGE_SCHEMA_SQL = "group_message.sql" + +TABLE_NAMES = { + "user", + "post", + "follow", + "mute", + "like", + "dislike", + "report", + "trace", + "rec", + "comment.sql", + "comment_like.sql", + "comment_dislike.sql", + "product.sql", + "group", + "group_member", + "group_message", +} + + +def get_db_path() -> str: + # First check if the database path is set in environment variables + env_db_path = os.environ.get("OASIS_DB_PATH") + if env_db_path: + return env_db_path + + # If no environment variable is set, use the original default path + curr_file_path = osp.abspath(__file__) + parent_dir = osp.dirname(osp.dirname(curr_file_path)) + db_dir = osp.join(parent_dir, DB_DIR) + os.makedirs(db_dir, exist_ok=True) + db_path = osp.join(db_dir, DB_NAME) + return db_path + + +def get_schema_dir_path() -> str: + curr_file_path = osp.abspath(__file__) + parent_dir = osp.dirname(osp.dirname(curr_file_path)) + schema_dir = osp.join(parent_dir, SCHEMA_DIR) + return schema_dir + + +def create_db(db_path: str | None = None): + r"""Create the database if it does not exist. A :obj:`twitter.db` + file will be automatically created in the :obj:`data` directory. + """ + schema_dir = get_schema_dir_path() + if db_path is None: + db_path = get_db_path() + + # Connect to the database: + print("db_path", db_path) + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + try: + # Read and execute the user table SQL script: + user_sql_path = osp.join(schema_dir, USER_SCHEMA_SQL) + with open(user_sql_path, "r") as sql_file: + user_sql_script = sql_file.read() + cursor.executescript(user_sql_script) + + # Read and execute the post table SQL script: + post_sql_path = osp.join(schema_dir, POST_SCHEMA_SQL) + with open(post_sql_path, "r") as sql_file: + post_sql_script = sql_file.read() + cursor.executescript(post_sql_script) + + # Read and execute the follow table SQL script: + follow_sql_path = osp.join(schema_dir, FOLLOW_SCHEMA_SQL) + with open(follow_sql_path, "r") as sql_file: + follow_sql_script = sql_file.read() + cursor.executescript(follow_sql_script) + + # Read and execute the mute table SQL script: + mute_sql_path = osp.join(schema_dir, MUTE_SCHEMA_SQL) + with open(mute_sql_path, "r") as sql_file: + mute_sql_script = sql_file.read() + cursor.executescript(mute_sql_script) + + # Read and execute the like table SQL script: + like_sql_path = osp.join(schema_dir, LIKE_SCHEMA_SQL) + with open(like_sql_path, "r") as sql_file: + like_sql_script = sql_file.read() + cursor.executescript(like_sql_script) + + # Read and execute the dislike table SQL script: + dislike_sql_path = osp.join(schema_dir, DISLIKE_SCHEMA_SQL) + with open(dislike_sql_path, "r") as sql_file: + dislike_sql_script = sql_file.read() + cursor.executescript(dislike_sql_script) + + # Read and execute the report table SQL script: + report_sql_path = osp.join(schema_dir, REPORT_SCHEAM_SQL) + with open(report_sql_path, "r") as sql_file: + report_sql_script = sql_file.read() + cursor.executescript(report_sql_script) + + # Read and execute the trace table SQL script: + trace_sql_path = osp.join(schema_dir, TRACE_SCHEMA_SQL) + with open(trace_sql_path, "r") as sql_file: + trace_sql_script = sql_file.read() + cursor.executescript(trace_sql_script) + + # Read and execute the rec table SQL script: + rec_sql_path = osp.join(schema_dir, REC_SCHEMA_SQL) + with open(rec_sql_path, "r") as sql_file: + rec_sql_script = sql_file.read() + cursor.executescript(rec_sql_script) + + # Read and execute the comment table SQL script: + comment_sql_path = osp.join(schema_dir, COMMENT_SCHEMA_SQL) + with open(comment_sql_path, "r") as sql_file: + comment_sql_script = sql_file.read() + cursor.executescript(comment_sql_script) + + # Read and execute the comment_like table SQL script: + comment_like_sql_path = osp.join(schema_dir, COMMENT_LIKE_SCHEMA_SQL) + with open(comment_like_sql_path, "r") as sql_file: + comment_like_sql_script = sql_file.read() + cursor.executescript(comment_like_sql_script) + + # Read and execute the comment_dislike table SQL script: + comment_dislike_sql_path = osp.join(schema_dir, + COMMENT_DISLIKE_SCHEMA_SQL) + with open(comment_dislike_sql_path, "r") as sql_file: + comment_dislike_sql_script = sql_file.read() + cursor.executescript(comment_dislike_sql_script) + + # Read and execute the product table SQL script: + product_sql_path = osp.join(schema_dir, PRODUCT_SCHEMA_SQL) + with open(product_sql_path, "r") as sql_file: + product_sql_script = sql_file.read() + cursor.executescript(product_sql_script) + + # Read and execute the group table SQL script: + group_sql_path = osp.join(schema_dir, GROUP_SCHEMA_SQL) + with open(group_sql_path, "r") as sql_file: + group_sql_script = sql_file.read() + cursor.executescript(group_sql_script) + + # Read and execute the group_member table SQL script: + group_member_sql_path = osp.join(schema_dir, GROUP_MEMBER_SCHEMA_SQL) + with open(group_member_sql_path, "r") as sql_file: + group_member_sql_script = sql_file.read() + cursor.executescript(group_member_sql_script) + + # Read and execute the group_message table SQL script: + group_message_sql_path = osp.join(schema_dir, GROUP_MESSAGE_SCHEMA_SQL) + with open(group_message_sql_path, "r") as sql_file: + group_message_sql_script = sql_file.read() + cursor.executescript(group_message_sql_script) + + # Commit the changes: + conn.commit() + + except sqlite3.Error as e: + print(f"An error occurred while creating tables: {e}") + + return conn, cursor + + +def print_db_tables_summary(): + # Connect to the SQLite database + db_path = get_db_path() + conn = sqlite3.connect(db_path) + cursor = conn.cursor() + + # Retrieve a list of all tables in the database + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + + # Print a summary of each table + for table in tables: + table_name = table[0] + if table_name not in TABLE_NAMES: + continue + print(f"Table: {table_name}") + + # Retrieve the table schema + cursor.execute(f"PRAGMA table_info({table_name})") + columns = cursor.fetchall() + column_names = [column[1] for column in columns] + print("- Columns:", column_names) + + # Retrieve and print foreign key information + cursor.execute(f"PRAGMA foreign_key_list({table_name})") + foreign_keys = cursor.fetchall() + if foreign_keys: + print("- Foreign Keys:") + for fk in foreign_keys: + print(f" {fk[2]} references {fk[3]}({fk[4]}) on update " + f"{fk[5]} on delete {fk[6]}") + else: + print(" No foreign keys.") + + # Print the first few rows of the table + cursor.execute(f"SELECT * FROM {table_name} LIMIT 5;") + rows = cursor.fetchall() + for row in rows: + print(row) + print() # Adds a newline for better readability between tables + + # Close the database connection + conn.close() + + +def fetch_table_from_db(cursor: sqlite3.Cursor, + table_name: str) -> List[Dict[str, Any]]: + cursor.execute(f"SELECT * FROM {table_name}") + columns = [description[0] for description in cursor.description] + data_dicts = [dict(zip(columns, row)) for row in cursor.fetchall()] + return data_dicts + + +def fetch_rec_table_as_matrix(cursor: sqlite3.Cursor) -> List[List[int]]: + # First, query all user_ids from the user table, assuming they start from + # 1 and are consecutive + cursor.execute("SELECT user_id FROM user ORDER BY user_id") + user_ids = [row[0] for row in cursor.fetchall()] + + # Then, query all records from the rec table + cursor.execute( + "SELECT user_id, post_id FROM rec ORDER BY user_id, post_id") + rec_rows = cursor.fetchall() + # Initialize a dictionary, assigning an empty list to each user_id + user_posts = {user_id: [] for user_id in user_ids} + # Fill the dictionary with the records queried from the rec table + for user_id, post_id in rec_rows: + if user_id in user_posts: + user_posts[user_id].append(post_id) + # Convert the dictionary into matrix form + matrix = [user_posts[user_id] for user_id in user_ids] + return matrix + + +def insert_matrix_into_rec_table(cursor: sqlite3.Cursor, + matrix: List[List[int]]) -> None: + # Iterate through the matrix, skipping the placeholder at index 0 + for user_id, post_ids in enumerate(matrix, start=1): + # Adjusted to start counting from 1 + for post_id in post_ids: + # Insert each combination of user_id and post_id into the rec table + cursor.execute("INSERT INTO rec (user_id, post_id) VALUES (?, ?)", + (user_id, post_id)) + + +if __name__ == "__main__": + create_db() + print_db_tables_summary() diff --git a/backend/vendor/camel-oasis/oasis/social_platform/platform.py b/backend/vendor/camel-oasis/oasis/social_platform/platform.py new file mode 100644 index 00000000..672e4bbb --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/platform.py @@ -0,0 +1,1642 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from __future__ import annotations + +import asyncio +import logging +import os +import random +import sqlite3 +import sys +from datetime import datetime, timedelta +from typing import Any + +from oasis.clock.clock import Clock +from oasis.social_platform.channel import Channel +from oasis.social_platform.database import (create_db, + fetch_rec_table_as_matrix, + fetch_table_from_db) +from oasis.social_platform.platform_utils import PlatformUtils +from oasis.social_platform.recsys import (rec_sys_personalized_twh, + rec_sys_personalized_with_trace, + rec_sys_random, rec_sys_reddit) +from oasis.social_platform.typing import ActionType, RecsysType + +# Create log directory if it doesn't exist +log_dir = "./log" +if not os.path.exists(log_dir): + os.makedirs(log_dir) + +if "sphinx" not in sys.modules: + twitter_log = logging.getLogger(name="social.twitter") + twitter_log.setLevel("DEBUG") + now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + file_handler = logging.FileHandler(f"./log/social.twitter-{now}.log") + file_handler.setLevel("DEBUG") + file_handler.setFormatter( + logging.Formatter( + "%(levelname)s - %(asctime)s - %(name)s - %(message)s")) + twitter_log.addHandler(file_handler) + + +class Platform: + r"""Platform.""" + + def __init__( + self, + db_path: str, + channel: Any = None, + sandbox_clock: Clock | None = None, + start_time: datetime | None = None, + show_score: bool = False, + allow_self_rating: bool = True, + recsys_type: str | RecsysType = "reddit", + refresh_rec_post_count: int = 1, + max_rec_post_len: int = 2, + following_post_count=3, + use_openai_embedding: bool = False, + ): + self.db_path = db_path + self.recsys_type = recsys_type + # import pdb; pdb.set_trace() + + # If no clock is specified, default the platform's time + # magnification factor to 60 + if sandbox_clock is None: + sandbox_clock = Clock(60) + if start_time is None: + start_time = datetime.now() + self.start_time = start_time + self.sandbox_clock = sandbox_clock + + self.db, self.db_cursor = create_db(self.db_path) + self.db.execute("PRAGMA synchronous = OFF") + + self.channel = channel or Channel() + + self.recsys_type = RecsysType(recsys_type) + + # Whether to simulate showing scores like Reddit (likes minus dislikes) + # instead of showing likes and dislikes separately + self.show_score = show_score + + # Whether to allow users to like or dislike their own posts and + # comments + self.allow_self_rating = allow_self_rating + + # The number of posts returned by the social media internal + # recommendation system per refresh + self.refresh_rec_post_count = refresh_rec_post_count + # The number of posts returned at once from posts made by followed + # users, ranked by like counts + self.following_post_count = following_post_count + # The maximum number of posts per user in the recommendation + # table (buffer) + self.max_rec_post_len = max_rec_post_len + # rec prob between random and personalized + self.rec_prob = 0.7 + self.use_openai_embedding = use_openai_embedding + + # Parameters for the platform's internal trending rules + self.trend_num_days = 7 + self.trend_top_k = 1 + + # Report threshold setting + self.report_threshold = 2 + + self.pl_utils = PlatformUtils( + self.db, + self.db_cursor, + self.start_time, + self.sandbox_clock, + self.show_score, + self.recsys_type, + self.report_threshold, + ) + + async def running(self): + while True: + message_id, data = await self.channel.receive_from() + + agent_id, message, action = data + action = ActionType(action) + + if action == ActionType.EXIT: + # If the database is in-memory, save it to a file before + # losing + if self.db_path == ":memory:": + dst = sqlite3.connect("mock.db") + with dst: + self.db.backup(dst) + + self.db_cursor.close() + self.db.close() + break + + # Retrieve the corresponding function using getattr + action_function = getattr(self, action.value, None) + if action_function: + # Get the names of the parameters of the function + func_code = action_function.__code__ + param_names = func_code.co_varnames[:func_code.co_argcount] + + len_param_names = len(param_names) + if len_param_names > 3: + raise ValueError( + f"Functions with {len_param_names} parameters are not " + f"supported.") + # Build a dictionary of parameters + params = {} + if len_param_names >= 2: + params["agent_id"] = agent_id + if len_param_names == 3: + # Assuming the second element in param_names is the name + # of the second parameter you want to add + second_param_name = param_names[2] + params[second_param_name] = message + + # Call the function with the parameters + result = await action_function(**params) + await self.channel.send_to((message_id, agent_id, result)) + else: + raise ValueError(f"Action {action} is not supported") + + def run(self): + asyncio.run(self.running()) + + async def sign_up(self, agent_id, user_message): + user_name, name, bio = user_message + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_insert_query = ( + "INSERT INTO user (user_id, agent_id, user_name, name, " + "bio, created_at, num_followings, num_followers) VALUES " + "(?, ?, ?, ?, ?, ?, ?, ?)") + self.pl_utils._execute_db_command( + user_insert_query, + (agent_id, agent_id, user_name, name, bio, current_time, 0, 0), + commit=True, + ) + user_id = agent_id + + action_info = {"name": name, "user_name": user_name, "bio": bio} + self.pl_utils._record_trace(user_id, ActionType.SIGNUP.value, + action_info, current_time) + # twitter_log.info(f"Trace inserted: user_id={user_id}, " + # f"current_time={current_time}, " + # f"action={ActionType.SIGNUP.value}, " + # f"info={action_info}") + return {"success": True, "user_id": user_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def sign_up_product(self, product_id: int, product_name: str): + # Note: do not sign up the product with the same product name + try: + product_insert_query = ( + "INSERT INTO product (product_id, product_name) VALUES (?, ?)") + self.pl_utils._execute_db_command(product_insert_query, + (product_id, product_name), + commit=True) + return {"success": True, "product_id": product_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def purchase_product(self, agent_id, purchase_message): + product_name, purchase_num = purchase_message + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + # try: + user_id = agent_id + # Check if a like record already exists + product_check_query = ( + "SELECT * FROM 'product' WHERE product_name = ?") + self.pl_utils._execute_db_command(product_check_query, + (product_name, )) + check_result = self.db_cursor.fetchone() + if not check_result: + # Product not found + return {"success": False, "error": "No such product."} + else: + product_id = check_result[0] + + product_update_query = ( + "UPDATE product SET sales = sales + ? WHERE product_name = ?") + self.pl_utils._execute_db_command(product_update_query, + (purchase_num, product_name), + commit=True) + + # Record the action in the trace table + action_info = { + "product_name": product_name, + "purchase_num": purchase_num + } + self.pl_utils._record_trace(user_id, ActionType.PURCHASE_PRODUCT.value, + action_info, current_time) + return {"success": True, "product_id": product_id} + # except Exception as e: + # return {"success": False, "error": str(e)} + + async def refresh(self, agent_id: int): + # Retrieve posts for a specific id from the rec table + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + # Retrieve all post_ids for a given user_id from the rec table + rec_query = "SELECT post_id FROM rec WHERE user_id = ?" + self.pl_utils._execute_db_command(rec_query, (user_id, )) + rec_results = self.db_cursor.fetchall() + + post_ids = [row[0] for row in rec_results] + selected_post_ids = post_ids + # If the number of post_ids >= self.refresh_rec_post_count, + # randomly select a specified number of post_ids + if len(selected_post_ids) >= self.refresh_rec_post_count: + selected_post_ids = random.sample(selected_post_ids, + self.refresh_rec_post_count) + + if self.recsys_type != RecsysType.REDDIT: + # Retrieve posts from following (in network) + # Modify the SQL query so that the refresh gets posts from + # people the user follows, sorted by the number of likes on + # Twitter + query_following_post = ( + "SELECT post.post_id, post.user_id, post.content, " + "post.created_at, post.num_likes FROM post " + "JOIN follow ON post.user_id = follow.followee_id " + "WHERE follow.follower_id = ? " + "ORDER BY post.num_likes DESC " + "LIMIT ?") + self.pl_utils._execute_db_command( + query_following_post, + ( + user_id, + self.following_post_count, + ), + ) + + following_posts = self.db_cursor.fetchall() + following_posts_ids = [row[0] for row in following_posts] + + selected_post_ids = following_posts_ids + selected_post_ids + selected_post_ids = list(set(selected_post_ids)) + + placeholders = ", ".join("?" for _ in selected_post_ids) + + post_query = ( + f"SELECT post_id, user_id, original_post_id, content, " + f"quote_content, created_at, num_likes, num_dislikes, " + f"num_shares FROM post WHERE post_id IN ({placeholders})") + self.pl_utils._execute_db_command(post_query, selected_post_ids) + results = self.db_cursor.fetchall() + if not results: + return {"success": False, "message": "No posts found."} + results_with_comments = self.pl_utils._add_comments_to_posts( + results) + + action_info = {"posts": results_with_comments} + # twitter_log.info(action_info) + self.pl_utils._record_trace(user_id, ActionType.REFRESH.value, + action_info, current_time) + + return {"success": True, "posts": results_with_comments} + except Exception as e: + return {"success": False, "error": str(e)} + + async def update_rec_table(self): + # Recsys(trace/user/post table), refresh rec table + twitter_log.info("Starting to refresh recommendation system cache...") + user_table = fetch_table_from_db(self.db_cursor, "user") + post_table = fetch_table_from_db(self.db_cursor, "post") + trace_table = fetch_table_from_db(self.db_cursor, "trace") + rec_matrix = fetch_rec_table_as_matrix(self.db_cursor) + + if self.recsys_type == RecsysType.RANDOM: + new_rec_matrix = rec_sys_random(post_table, rec_matrix, + self.max_rec_post_len) + elif self.recsys_type == RecsysType.TWITTER: + new_rec_matrix = rec_sys_personalized_with_trace( + user_table, post_table, trace_table, rec_matrix, + self.max_rec_post_len) + elif self.recsys_type == RecsysType.TWHIN: + try: + latest_post_time = post_table[-1]["created_at"] + second_latest_post_time = post_table[-2]["created_at"] if len( + post_table) > 1 else latest_post_time + post_query = """ + SELECT COUNT(*) + FROM post + WHERE created_at = ? OR created_at = ? + """ + self.pl_utils._execute_db_command( + post_query, (latest_post_time, second_latest_post_time)) + result = self.db_cursor.fetchone() + latest_post_count = result[0] + if not latest_post_count: + return { + "success": False, + "message": "Fail to get latest posts count" + } + new_rec_matrix = rec_sys_personalized_twh( + user_table, + post_table, + latest_post_count, + trace_table, + rec_matrix, + self.max_rec_post_len, + self.sandbox_clock.time_step, + use_openai_embedding=self.use_openai_embedding, + ) + except Exception as e: + twitter_log.error(e) + # If no post in the platform, skip updating the rec table + return + elif self.recsys_type == RecsysType.REDDIT: + new_rec_matrix = rec_sys_reddit(post_table, rec_matrix, + self.max_rec_post_len) + else: + raise ValueError("Unsupported recommendation system type, please " + "check the `RecsysType`.") + + sql_query = "DELETE FROM rec" + # Execute the SQL statement using the _execute_db_command function + self.pl_utils._execute_db_command(sql_query, commit=True) + + # Batch insertion is more time-efficient + # create a list of values to insert + insert_values = [(user_id, post_id) + for user_id in range(len(new_rec_matrix)) + for post_id in new_rec_matrix[user_id]] + + # Perform batch insertion into the database + self.pl_utils._execute_many_db_command( + "INSERT INTO rec (user_id, post_id) VALUES (?, ?)", + insert_values, + commit=True, + ) + + async def create_post(self, agent_id: int, content: str): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + post_insert_query = ( + "INSERT INTO post (user_id, content, created_at, num_likes, " + "num_dislikes, num_shares) VALUES (?, ?, ?, ?, ?, ?)") + self.pl_utils._execute_db_command( + post_insert_query, (user_id, content, current_time, 0, 0, 0), + commit=True) + post_id = self.db_cursor.lastrowid + + action_info = {"content": content, "post_id": post_id} + self.pl_utils._record_trace(user_id, ActionType.CREATE_POST.value, + action_info, current_time) + + # twitter_log.info(f"Trace inserted: user_id={user_id}, " + # f"current_time={current_time}, " + # f"action={ActionType.CREATE_POST.value}, " + # f"info={action_info}") + return {"success": True, "post_id": post_id} + + except Exception as e: + return {"success": False, "error": str(e)} + + async def repost(self, agent_id: int, post_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + # Ensure the content has not been reposted by this user before + repost_check_query = ( + "SELECT * FROM 'post' WHERE original_post_id = ? AND " + "user_id = ?") + self.pl_utils._execute_db_command(repost_check_query, + (post_id, user_id)) + if self.db_cursor.fetchone(): + # for common and quote post, check if the post has been + # reposted + return { + "success": False, + "error": "Repost record already exists." + } + + post_type_result = self.pl_utils._get_post_type(post_id) + post_insert_query = ("INSERT INTO post (user_id, original_post_id" + ", created_at) VALUES (?, ?, ?)") + # Update num_shares for the found post + update_shares_query = ( + "UPDATE post SET num_shares = num_shares + 1 WHERE post_id = ?" + ) + + if not post_type_result: + return {"success": False, "error": "Post not found."} + elif (post_type_result['type'] == 'common' + or post_type_result['type'] == 'quote'): + self.pl_utils._execute_db_command( + post_insert_query, (user_id, post_id, current_time), + commit=True) + self.pl_utils._execute_db_command(update_shares_query, + (post_id, ), + commit=True) + elif post_type_result['type'] == 'repost': + repost_check_query = ( + "SELECT * FROM 'post' WHERE original_post_id = ? AND " + "user_id = ?") + self.pl_utils._execute_db_command( + repost_check_query, + (post_type_result['root_post_id'], user_id)) + + if self.db_cursor.fetchone(): + # for repost post, check if the post has been reposted + return { + "success": False, + "error": "Repost record already exists." + } + + self.pl_utils._execute_db_command( + post_insert_query, + (user_id, post_type_result['root_post_id'], current_time), + commit=True) + self.pl_utils._execute_db_command( + update_shares_query, (post_type_result['root_post_id'], ), + commit=True) + + new_post_id = self.db_cursor.lastrowid + + action_info = {"reposted_id": post_id, "new_post_id": new_post_id} + self.pl_utils._record_trace(user_id, ActionType.REPOST.value, + action_info, current_time) + + return {"success": True, "post_id": new_post_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def quote_post(self, agent_id: int, quote_message: tuple): + post_id, quote_content = quote_message + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + # Allow quote a post more than once because the quote content may + # be different + + post_query = "SELECT content FROM post WHERE post_id = ?" + + post_type_result = self.pl_utils._get_post_type(post_id) + post_insert_query = ( + "INSERT INTO post (user_id, original_post_id, " + "content, quote_content, created_at) VALUES (?, ?, ?, ?, ?)") + update_shares_query = ( + "UPDATE post SET num_shares = num_shares + 1 WHERE post_id = ?" + ) + + if not post_type_result: + return {"success": False, "error": "Post not found."} + elif post_type_result['type'] == 'common': + self.pl_utils._execute_db_command(post_query, (post_id, )) + post_content = self.db_cursor.fetchone()[0] + self.pl_utils._execute_db_command( + post_insert_query, (user_id, post_id, post_content, + quote_content, current_time), + commit=True) + self.pl_utils._execute_db_command(update_shares_query, + (post_id, ), + commit=True) + elif (post_type_result['type'] == 'repost' + or post_type_result['type'] == 'quote'): + self.pl_utils._execute_db_command( + post_query, (post_type_result['root_post_id'], )) + post_content = self.db_cursor.fetchone()[0] + self.pl_utils._execute_db_command( + post_insert_query, + (user_id, post_type_result['root_post_id'], post_content, + quote_content, current_time), + commit=True) + self.pl_utils._execute_db_command( + update_shares_query, (post_type_result['root_post_id'], ), + commit=True) + + new_post_id = self.db_cursor.lastrowid + + action_info = {"quoted_id": post_id, "new_post_id": new_post_id} + self.pl_utils._record_trace(user_id, ActionType.QUOTE_POST.value, + action_info, current_time) + + return {"success": True, "post_id": new_post_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def like_post(self, agent_id: int, post_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + post_type_result = self.pl_utils._get_post_type(post_id) + if post_type_result['type'] == 'repost': + post_id = post_type_result['root_post_id'] + user_id = agent_id + # Check if a like record already exists + like_check_query = ("SELECT * FROM 'like' WHERE post_id = ? AND " + "user_id = ?") + self.pl_utils._execute_db_command(like_check_query, + (post_id, user_id)) + if self.db_cursor.fetchone(): + # Like record already exists + return { + "success": False, + "error": "Like record already exists." + } + + # Check if the post to be liked is self-posted + if self.allow_self_rating is False: + check_result = self.pl_utils._check_self_post_rating( + post_id, user_id) + if check_result: + return check_result + + # Update the number of likes in the post table + post_update_query = ( + "UPDATE post SET num_likes = num_likes + 1 WHERE post_id = ?") + self.pl_utils._execute_db_command(post_update_query, (post_id, ), + commit=True) + + # Add a record in the like table + like_insert_query = ( + "INSERT INTO 'like' (post_id, user_id, created_at) " + "VALUES (?, ?, ?)") + self.pl_utils._execute_db_command(like_insert_query, + (post_id, user_id, current_time), + commit=True) + # Get the ID of the newly inserted like record + like_id = self.db_cursor.lastrowid + + # Record the action in the trace table + # if post has been reposted, record the root post id into trace + action_info = {"post_id": post_id, "like_id": like_id} + self.pl_utils._record_trace(user_id, ActionType.LIKE_POST.value, + action_info, current_time) + return {"success": True, "like_id": like_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def unlike_post(self, agent_id: int, post_id: int): + try: + post_type_result = self.pl_utils._get_post_type(post_id) + if post_type_result['type'] == 'repost': + post_id = post_type_result['root_post_id'] + user_id = agent_id + + # Check if a like record already exists + like_check_query = ("SELECT * FROM 'like' WHERE post_id = ? AND " + "user_id = ?") + self.pl_utils._execute_db_command(like_check_query, + (post_id, user_id)) + result = self.db_cursor.fetchone() + + if not result: + # No like record exists + return { + "success": False, + "error": "Like record does not exist." + } + + # Get the `like_id` + like_id, _, _, _ = result + + # Update the number of likes in the post table + post_update_query = ( + "UPDATE post SET num_likes = num_likes - 1 WHERE post_id = ?") + self.pl_utils._execute_db_command( + post_update_query, + (post_id, ), + commit=True, + ) + + # Delete the record in the like table + like_delete_query = "DELETE FROM 'like' WHERE like_id = ?" + self.pl_utils._execute_db_command( + like_delete_query, + (like_id, ), + commit=True, + ) + + # Record the action in the trace table + action_info = {"post_id": post_id, "like_id": like_id} + self.pl_utils._record_trace(user_id, ActionType.UNLIKE_POST.value, + action_info) + return {"success": True, "like_id": like_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def dislike_post(self, agent_id: int, post_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + post_type_result = self.pl_utils._get_post_type(post_id) + if post_type_result['type'] == 'repost': + post_id = post_type_result['root_post_id'] + user_id = agent_id + # Check if a dislike record already exists + like_check_query = ( + "SELECT * FROM 'dislike' WHERE post_id = ? AND user_id = ?") + self.pl_utils._execute_db_command(like_check_query, + (post_id, user_id)) + if self.db_cursor.fetchone(): + # Dislike record already exists + return { + "success": False, + "error": "Dislike record already exists." + } + + # Check if the post to be disliked is self-posted + if self.allow_self_rating is False: + check_result = self.pl_utils._check_self_post_rating( + post_id, user_id) + if check_result: + return check_result + + # Update the number of dislikes in the post table + post_update_query = ( + "UPDATE post SET num_dislikes = num_dislikes + 1 WHERE " + "post_id = ?") + self.pl_utils._execute_db_command(post_update_query, (post_id, ), + commit=True) + + # Add a record in the dislike table + dislike_insert_query = ( + "INSERT INTO 'dislike' (post_id, user_id, created_at) " + "VALUES (?, ?, ?)") + self.pl_utils._execute_db_command(dislike_insert_query, + (post_id, user_id, current_time), + commit=True) + # Get the ID of the newly inserted dislike record + dislike_id = self.db_cursor.lastrowid + + # Record the action in the trace table + action_info = {"post_id": post_id, "dislike_id": dislike_id} + self.pl_utils._record_trace(user_id, ActionType.DISLIKE_POST.value, + action_info, current_time) + return {"success": True, "dislike_id": dislike_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def undo_dislike_post(self, agent_id: int, post_id: int): + try: + post_type_result = self.pl_utils._get_post_type(post_id) + if post_type_result['type'] == 'repost': + post_id = post_type_result['root_post_id'] + user_id = agent_id + + # Check if a dislike record already exists + like_check_query = ( + "SELECT * FROM 'dislike' WHERE post_id = ? AND user_id = ?") + self.pl_utils._execute_db_command(like_check_query, + (post_id, user_id)) + result = self.db_cursor.fetchone() + + if not result: + # No dislike record exists + return { + "success": False, + "error": "Dislike record does not exist." + } + + # Get the `dislike_id` + dislike_id, _, _, _ = result + + # Update the number of dislikes in the post table + post_update_query = ( + "UPDATE post SET num_dislikes = num_dislikes - 1 WHERE " + "post_id = ?") + self.pl_utils._execute_db_command( + post_update_query, + (post_id, ), + commit=True, + ) + + # Delete the record in the dislike table + like_delete_query = "DELETE FROM 'dislike' WHERE dislike_id = ?" + self.pl_utils._execute_db_command( + like_delete_query, + (dislike_id, ), + commit=True, + ) + + # Record the action in the trace table + action_info = {"post_id": post_id, "dislike_id": dislike_id} + self.pl_utils._record_trace(user_id, + ActionType.UNDO_DISLIKE_POST.value, + action_info) + return {"success": True, "dislike_id": dislike_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def search_posts(self, agent_id: int, query: str): + try: + user_id = agent_id + # Update the SQL query to search by content, post_id, and user_id + # simultaneously + sql_query = ( + "SELECT post_id, user_id, original_post_id, content, " + "quote_content, created_at, num_likes, num_dislikes, " + "num_shares FROM post WHERE content LIKE ? OR CAST(post_id AS " + "TEXT) LIKE ? OR CAST(user_id AS TEXT) LIKE ?") + # Note: CAST is necessary because post_id and user_id are integers, + # while the search query is a string type + self.pl_utils._execute_db_command( + sql_query, + ("%" + query + "%", "%" + query + "%", "%" + query + "%"), + commit=True, + ) + results = self.db_cursor.fetchall() + + # Record the operation in the trace table + action_info = {"query": query} + self.pl_utils._record_trace(user_id, ActionType.SEARCH_POSTS.value, + action_info) + + # If no results are found, return a dictionary indicating failure + if not results: + return { + "success": False, + "message": "No posts found matching the query.", + } + results_with_comments = self.pl_utils._add_comments_to_posts( + results) + + return {"success": True, "posts": results_with_comments} + except Exception as e: + return {"success": False, "error": str(e)} + + async def search_user(self, agent_id: int, query: str): + try: + user_id = agent_id + sql_query = ( + "SELECT user_id, user_name, name, bio, created_at, " + "num_followings, num_followers " + "FROM user " + "WHERE user_name LIKE ? OR name LIKE ? OR bio LIKE ? OR " + "CAST(user_id AS TEXT) LIKE ?") + # Rewrite to use the execute_db_command method + self.pl_utils._execute_db_command( + sql_query, + ( + "%" + query + "%", + "%" + query + "%", + "%" + query + "%", + "%" + query + "%", + ), + commit=True, + ) + results = self.db_cursor.fetchall() + + # Record the operation in the trace table + action_info = {"query": query} + self.pl_utils._record_trace(user_id, ActionType.SEARCH_USER.value, + action_info) + + # If no results are found, return a dict indicating failure + if not results: + return { + "success": False, + "message": "No users found matching the query.", + } + + # Convert each tuple in results into a dictionary + users = [{ + "user_id": user_id, + "user_name": user_name, + "name": name, + "bio": bio, + "created_at": created_at, + "num_followings": num_followings, + "num_followers": num_followers, + } for user_id, user_name, name, bio, created_at, num_followings, + num_followers in results] + return {"success": True, "users": users} + except Exception as e: + return {"success": False, "error": str(e)} + + async def follow(self, agent_id: int, followee_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + # Check if a follow record already exists + follow_check_query = ("SELECT * FROM follow WHERE follower_id = ? " + "AND followee_id = ?") + self.pl_utils._execute_db_command(follow_check_query, + (user_id, followee_id)) + if self.db_cursor.fetchone(): + # Follow record already exists + return { + "success": False, + "error": "Follow record already exists." + } + + # Add a record in the follow table + follow_insert_query = ( + "INSERT INTO follow (follower_id, followee_id, created_at) " + "VALUES (?, ?, ?)") + self.pl_utils._execute_db_command( + follow_insert_query, (user_id, followee_id, current_time), + commit=True) + # Get the ID of the newly inserted follow record + follow_id = self.db_cursor.lastrowid + + # Update the following field in the user table + user_update_query1 = ( + "UPDATE user SET num_followings = num_followings + 1 " + "WHERE user_id = ?") + self.pl_utils._execute_db_command(user_update_query1, (user_id, ), + commit=True) + + # Update the follower field in the user table + user_update_query2 = ( + "UPDATE user SET num_followers = num_followers + 1 " + "WHERE user_id = ?") + self.pl_utils._execute_db_command(user_update_query2, + (followee_id, ), + commit=True) + + # Record the operation in the trace table + action_info = {"follow_id": follow_id} + self.pl_utils._record_trace(user_id, ActionType.FOLLOW.value, + action_info, current_time) + # twitter_log.info(f"Trace inserted: user_id={user_id}, " + # f"current_time={current_time}, " + # f"action={ActionType.FOLLOW.value}, " + # f"info={action_info}") + return {"success": True, "follow_id": follow_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def unfollow(self, agent_id: int, followee_id: int): + try: + user_id = agent_id + # Check for the existence of a follow record and get its ID + follow_check_query = ( + "SELECT follow_id FROM follow WHERE follower_id = ? AND " + "followee_id = ?") + self.pl_utils._execute_db_command(follow_check_query, + (user_id, followee_id)) + follow_record = self.db_cursor.fetchone() + if not follow_record: + return { + "success": False, + "error": "Follow record does not exist." + } + # Assuming ID is in the first column of the result + follow_id = follow_record[0] + + # Delete the record in the follow table + follow_delete_query = "DELETE FROM follow WHERE follow_id = ?" + self.pl_utils._execute_db_command(follow_delete_query, + (follow_id, ), + commit=True) + + # Update the following field in the user table + user_update_query1 = ( + "UPDATE user SET num_followings = num_followings - 1 " + "WHERE user_id = ?") + self.pl_utils._execute_db_command(user_update_query1, (user_id, ), + commit=True) + + # Update the follower field in the user table + user_update_query2 = ( + "UPDATE user SET num_followers = num_followers - 1 " + "WHERE user_id = ?") + self.pl_utils._execute_db_command(user_update_query2, + (followee_id, ), + commit=True) + + # Record the operation in the trace table + action_info = {"followee_id": followee_id} + self.pl_utils._record_trace(user_id, ActionType.UNFOLLOW.value, + action_info) + return { + "success": True, + "follow_id": follow_id, + } # Return the ID of the deleted follow record + except Exception as e: + return {"success": False, "error": str(e)} + + async def mute(self, agent_id: int, mutee_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + # Check if a mute record already exists + mute_check_query = ("SELECT * FROM mute WHERE muter_id = ? AND " + "mutee_id = ?") + self.pl_utils._execute_db_command(mute_check_query, + (user_id, mutee_id)) + if self.db_cursor.fetchone(): + # Mute record already exists + return { + "success": False, + "error": "Mute record already exists." + } + # Add a record in the mute table + mute_insert_query = ( + "INSERT INTO mute (muter_id, mutee_id, created_at) " + "VALUES (?, ?, ?)") + self.pl_utils._execute_db_command( + mute_insert_query, (user_id, mutee_id, current_time), + commit=True) + # Get the ID of the newly inserted mute record + mute_id = self.db_cursor.lastrowid + + # Record the operation in the trace table + action_info = {"mutee_id": mutee_id} + self.pl_utils._record_trace(user_id, ActionType.MUTE.value, + action_info, current_time) + return {"success": True, "mute_id": mute_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def unmute(self, agent_id: int, mutee_id: int): + try: + user_id = agent_id + # Check for the specified mute record and get mute_id + mute_check_query = ( + "SELECT mute_id FROM mute WHERE muter_id = ? AND mutee_id = ?") + self.pl_utils._execute_db_command(mute_check_query, + (user_id, mutee_id)) + mute_record = self.db_cursor.fetchone() + if not mute_record: + # If no mute record exists + return {"success": False, "error": "No mute record exists."} + mute_id = mute_record[0] + + # Delete the specified mute record from the mute table + mute_delete_query = "DELETE FROM mute WHERE mute_id = ?" + self.pl_utils._execute_db_command(mute_delete_query, (mute_id, ), + commit=True) + + # Record the unmute operation in the trace table + action_info = {"mutee_id": mutee_id} + self.pl_utils._record_trace(user_id, ActionType.UNMUTE.value, + action_info) + return {"success": True, "mute_id": mute_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def trend(self, agent_id: int): + """ + Get the top K trending posts in the last num_days days. + """ + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + # Calculate the start time for the search + if self.recsys_type == RecsysType.REDDIT: + start_time = current_time - timedelta(days=self.trend_num_days) + else: + start_time = int(current_time) - self.trend_num_days * 24 * 60 + + # Build the SQL query + sql_query = """ + SELECT post_id, user_id, original_post_id, content, + quote_content, created_at, num_likes, num_dislikes, + num_shares FROM post + WHERE created_at >= ? + ORDER BY num_likes DESC + LIMIT ? + """ + # Execute the database query + self.pl_utils._execute_db_command(sql_query, + (start_time, self.trend_top_k), + commit=True) + results = self.db_cursor.fetchall() + + # If no results were found, return a dictionary indicating failure + if not results: + return { + "success": False, + "message": "No trending posts in the specified period.", + } + results_with_comments = self.pl_utils._add_comments_to_posts( + results) + + action_info = {"posts": results_with_comments} + self.pl_utils._record_trace(user_id, ActionType.TREND.value, + action_info, current_time) + + return {"success": True, "posts": results_with_comments} + except Exception as e: + return {"success": False, "error": str(e)} + + async def create_comment(self, agent_id: int, comment_message: tuple): + post_id, content = comment_message + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + post_type_result = self.pl_utils._get_post_type(post_id) + if post_type_result['type'] == 'repost': + post_id = post_type_result['root_post_id'] + user_id = agent_id + + # Insert the comment record + comment_insert_query = ( + "INSERT INTO comment (post_id, user_id, content, created_at) " + "VALUES (?, ?, ?, ?)") + self.pl_utils._execute_db_command( + comment_insert_query, + (post_id, user_id, content, current_time), + commit=True, + ) + comment_id = self.db_cursor.lastrowid + + # Prepare information for the trace record + action_info = {"content": content, "comment_id": comment_id} + self.pl_utils._record_trace(user_id, + ActionType.CREATE_COMMENT.value, + action_info, current_time) + + return {"success": True, "comment_id": comment_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def like_comment(self, agent_id: int, comment_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + # Check if a like record already exists + like_check_query = ( + "SELECT * FROM comment_like WHERE comment_id = ? AND " + "user_id = ?") + self.pl_utils._execute_db_command(like_check_query, + (comment_id, user_id)) + if self.db_cursor.fetchone(): + # Like record already exists + return { + "success": False, + "error": "Comment like record already exists.", + } + + # Check if the comment to be liked was posted by oneself + if self.allow_self_rating is False: + check_result = self.pl_utils._check_self_comment_rating( + comment_id, user_id) + if check_result: + return check_result + + # Update the number of likes in the comment table + comment_update_query = ( + "UPDATE comment SET num_likes = num_likes + 1 WHERE " + "comment_id = ?") + self.pl_utils._execute_db_command(comment_update_query, + (comment_id, ), + commit=True) + + # Add a record in the comment_like table + like_insert_query = ( + "INSERT INTO comment_like (comment_id, user_id, created_at) " + "VALUES (?, ?, ?)") + self.pl_utils._execute_db_command( + like_insert_query, (comment_id, user_id, current_time), + commit=True) + # Get the ID of the newly inserted like record + comment_like_id = self.db_cursor.lastrowid + + # Record the operation in the trace table + action_info = { + "comment_id": comment_id, + "comment_like_id": comment_like_id + } + self.pl_utils._record_trace(user_id, ActionType.LIKE_COMMENT.value, + action_info, current_time) + return {"success": True, "comment_like_id": comment_like_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def unlike_comment(self, agent_id: int, comment_id: int): + try: + user_id = agent_id + + # Check if a like record already exists + like_check_query = ( + "SELECT * FROM comment_like WHERE comment_id = ? AND " + "user_id = ?") + self.pl_utils._execute_db_command(like_check_query, + (comment_id, user_id)) + result = self.db_cursor.fetchone() + + if not result: + # No like record exists + return { + "success": False, + "error": "Comment like record does not exist.", + } + # Get the `comment_like_id` + comment_like_id = result[0] + + # Update the number of likes in the comment table + comment_update_query = ( + "UPDATE comment SET num_likes = num_likes - 1 WHERE " + "comment_id = ?") + self.pl_utils._execute_db_command( + comment_update_query, + (comment_id, ), + commit=True, + ) + # Delete the record in the comment_like table + like_delete_query = ("DELETE FROM comment_like WHERE " + "comment_like_id = ?") + self.pl_utils._execute_db_command( + like_delete_query, + (comment_like_id, ), + commit=True, + ) + # Record the operation in the trace table + action_info = { + "comment_id": comment_id, + "comment_like_id": comment_like_id + } + self.pl_utils._record_trace(user_id, + ActionType.UNLIKE_COMMENT.value, + action_info) + return {"success": True, "comment_like_id": comment_like_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def dislike_comment(self, agent_id: int, comment_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + # Check if a dislike record already exists + dislike_check_query = ( + "SELECT * FROM comment_dislike WHERE comment_id = ? AND " + "user_id = ?") + self.pl_utils._execute_db_command(dislike_check_query, + (comment_id, user_id)) + if self.db_cursor.fetchone(): + # Dislike record already exists + return { + "success": False, + "error": "Comment dislike record already exists.", + } + + # Check if the comment to be disliked was posted by oneself + if self.allow_self_rating is False: + check_result = self.pl_utils._check_self_comment_rating( + comment_id, user_id) + if check_result: + return check_result + + # Update the number of dislikes in the comment table + comment_update_query = ( + "UPDATE comment SET num_dislikes = num_dislikes + 1 WHERE " + "comment_id = ?") + self.pl_utils._execute_db_command(comment_update_query, + (comment_id, ), + commit=True) + + # Add a record in the comment_dislike table + dislike_insert_query = ( + "INSERT INTO comment_dislike (comment_id, user_id, " + "created_at) VALUES (?, ?, ?)") + self.pl_utils._execute_db_command( + dislike_insert_query, (comment_id, user_id, current_time), + commit=True) + # Get the ID of the newly inserted dislike record + comment_dislike_id = (self.db_cursor.lastrowid) + + # Record the operation in the trace table + action_info = { + "comment_id": comment_id, + "comment_dislike_id": comment_dislike_id, + } + self.pl_utils._record_trace(user_id, + ActionType.DISLIKE_COMMENT.value, + action_info, current_time) + return {"success": True, "comment_dislike_id": comment_dislike_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def undo_dislike_comment(self, agent_id: int, comment_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + # Check if a dislike record already exists + dislike_check_query = ( + "SELECT comment_dislike_id FROM comment_dislike WHERE " + "comment_id = ? AND user_id = ?") + self.pl_utils._execute_db_command(dislike_check_query, + (comment_id, user_id)) + dislike_record = self.db_cursor.fetchone() + if not dislike_record: + # No dislike record exists + return { + "success": False, + "error": "Comment dislike record does not exist.", + } + comment_dislike_id = dislike_record[0] + + # Delete the record from the comment_dislike table + dislike_delete_query = ( + "DELETE FROM comment_dislike WHERE comment_id = ? AND " + "user_id = ?") + self.pl_utils._execute_db_command(dislike_delete_query, + (comment_id, user_id), + commit=True) + + # Update the number of dislikes in the comment table + comment_update_query = ( + "UPDATE comment SET num_dislikes = num_dislikes - 1 WHERE " + "comment_id = ?") + self.pl_utils._execute_db_command(comment_update_query, + (comment_id, ), + commit=True) + + # Record the operation in the trace table + action_info = { + "comment_id": comment_id, + "comment_dislike_id": comment_dislike_id, + } + self.pl_utils._record_trace(user_id, + ActionType.UNDO_DISLIKE_COMMENT.value, + action_info, current_time) + return {"success": True, "comment_dislike_id": comment_dislike_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def do_nothing(self, agent_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + action_info = {} + self.pl_utils._record_trace(user_id, ActionType.DO_NOTHING.value, + action_info, current_time) + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def interview(self, agent_id: int, interview_data): + """Interview an agent with the given prompt and record the response. + + Args: + agent_id (int): The ID of the agent being interviewed. + interview_data: Either a string (prompt only) or dict with prompt + and response. + + Returns: + dict: A dictionary with success status. + """ + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + # Handle both old format (string prompt) and new format + # (dict with prompt + response) + if isinstance(interview_data, str): + # Old format: just the prompt + prompt = interview_data + response = None + interview_id = f"{current_time}_{user_id}" + action_info = {"prompt": prompt, "interview_id": interview_id} + else: + # New format: dict with prompt and response + prompt = interview_data.get("prompt", "") + response = interview_data.get("response", "") + interview_id = f"{current_time}_{user_id}" + action_info = { + "prompt": prompt, + "response": response, + "interview_id": interview_id + } + + # Record the interview in the trace table + self.pl_utils._record_trace(user_id, ActionType.INTERVIEW.value, + action_info, current_time) + + return {"success": True, "interview_id": interview_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def report_post(self, agent_id: int, report_message: tuple): + post_id, report_reason = report_message + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + post_type_result = self.pl_utils._get_post_type(post_id) + + # Check if a report record already exists + check_report_query = ( + "SELECT * FROM report WHERE user_id = ? AND post_id = ?") + self.pl_utils._execute_db_command(check_report_query, + (user_id, post_id)) + if self.db_cursor.fetchone(): + return { + "success": False, + "error": "Report record already exists." + } + + if not post_type_result: + return {"success": False, "error": "Post not found."} + + # Update the number of reports in the post table + update_reports_query = ( + "UPDATE post SET num_reports = num_reports + 1 WHERE " + "post_id = ?") + self.pl_utils._execute_db_command(update_reports_query, + (post_id, ), + commit=True) + + # Add a report in the report table + report_insert_query = ( + "INSERT INTO report (post_id, user_id, report_reason, " + "created_at) VALUES (?, ?, ?, ?)") + self.pl_utils._execute_db_command( + report_insert_query, + (post_id, user_id, report_reason, current_time), + commit=True) + + # Get the ID of the newly inserted report record + report_id = self.db_cursor.lastrowid + + # Record the action in the trace table + action_info = {"post_id": post_id, "report_id": report_id} + self.pl_utils._record_trace(user_id, ActionType.REPORT_POST.value, + action_info, current_time) + + return {"success": True, "report_id": report_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def send_to_group(self, agent_id: int, message: tuple): + group_id, content = message + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + # check if user is a member of the group + check_query = ("SELECT * FROM group_members WHERE group_id = ? " + "AND agent_id = ?") + self.pl_utils._execute_db_command(check_query, (group_id, user_id)) + if not self.db_cursor.fetchone(): + return { + "success": False, + "error": "User is not a member of this group.", + } + + # Insert the message into the group_messages table + insert_query = """ + INSERT INTO group_messages + (group_id, sender_id, content, sent_at) + VALUES (?, ?, ?, ?) + """ + self.pl_utils._execute_db_command( + insert_query, (group_id, user_id, content, current_time), + commit=True) + message_id = self.db_cursor.lastrowid + + # get the group members + members_query = ("SELECT agent_id FROM group_members WHERE " + "group_id = ? AND agent_id != ?") + self.pl_utils._execute_db_command(members_query, + (group_id, user_id)) + members = [row[0] for row in self.db_cursor.fetchall()] + action_info = { + "group_id": group_id, + "message_id": message_id, + "content": content, + } + self.pl_utils._record_trace(user_id, + ActionType.SEND_TO_GROUP.value, + action_info, current_time) + + return {"success": True, "message_id": message_id, "to": members} + except Exception as e: + return {"success": False, "error": str(e)} + + async def create_group(self, agent_id: int, group_name: str): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + # insert the group into the groups table + insert_query = """ + INSERT INTO chat_group (name, created_at) VALUES (?, ?) + """ + self.pl_utils._execute_db_command(insert_query, + (group_name, current_time), + commit=True) + group_id = self.db_cursor.lastrowid + + # insert the user as a member of the group + join_query = """ + INSERT INTO group_members (group_id, agent_id, joined_at) + VALUES (?, ?, ?) + """ + self.pl_utils._execute_db_command( + join_query, (group_id, user_id, current_time), commit=True) + + action_info = {"group_id": group_id, "group_name": group_name} + self.pl_utils._record_trace(user_id, ActionType.CREATE_GROUP.value, + action_info, current_time) + + return {"success": True, "group_id": group_id} + except Exception as e: + return {"success": False, "error": str(e)} + + async def join_group(self, agent_id: int, group_id: int): + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + try: + user_id = agent_id + + # check if group exists + check_group_query = """SELECT * FROM chat_group + WHERE group_id = ?""" + self.pl_utils._execute_db_command(check_group_query, (group_id, )) + if not self.db_cursor.fetchone(): + return {"success": False, "error": "Group does not exist."} + + # check if user is already in the group + check_member_query = ( + "SELECT * FROM group_members WHERE group_id = ? " + "AND agent_id = ?") + self.pl_utils._execute_db_command(check_member_query, + (group_id, user_id)) + if self.db_cursor.fetchone(): + return { + "success": False, + "error": "User is already in the group." + } + + # join the group + join_query = """ + INSERT INTO group_members + (group_id, agent_id, joined_at) VALUES (?, ?, ?) + """ + self.pl_utils._execute_db_command( + join_query, (group_id, user_id, current_time), commit=True) + + action_info = {"group_id": group_id} + self.pl_utils._record_trace(user_id, ActionType.JOIN_GROUP.value, + action_info, current_time) + + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def leave_group(self, agent_id: int, group_id: int): + try: + user_id = agent_id + + # check if user is a member of the group + check_query = ("SELECT * FROM group_members " + "WHERE group_id = ? AND agent_id = ?") + self.pl_utils._execute_db_command(check_query, (group_id, user_id)) + if not self.db_cursor.fetchone(): + return { + "success": False, + "error": "User is not a member of this group." + } + + # delete the member record + delete_query = ("DELETE FROM group_members " + "WHERE group_id = ? AND agent_id = ?") + self.pl_utils._execute_db_command(delete_query, + (group_id, user_id), + commit=True) + + action_info = {"group_id": group_id} + self.pl_utils._record_trace(user_id, ActionType.LEAVE_GROUP.value, + action_info) + + return {"success": True} + except Exception as e: + return {"success": False, "error": str(e)} + + async def listen_from_group(self, agent_id: int): + try: + # get all groups Dict[group_id, group_name] + query = """ SELECT * FROM chat_group """ + self.pl_utils._execute_db_command(query) + all_groups = {} + for row in self.db_cursor.fetchall(): + all_groups[row[0]] = row[1] + + # get all groups that the user is a member of + in_query = """ + SELECT group_id FROM group_members WHERE agent_id = ? + """ + self.pl_utils._execute_db_command(in_query, (agent_id, )) + joined_group_ids = [row[0] for row in self.db_cursor.fetchall()] + + # get all messages from those groups, Dict[group_id, [messages]] + messages = {} + for group_id in joined_group_ids: + select_query = """ + SELECT message_id, content, sender_id, + sent_at FROM group_messages WHERE group_id = ? + """ + self.pl_utils._execute_db_command(select_query, (group_id, )) + messages[group_id] = [{ + "message_id": row[0], + "content": row[1], + "sender_id": row[2], + "sent_at": row[3], + } for row in self.db_cursor.fetchall()] + + return { + "success": True, + "all_groups": all_groups, + "joined_groups": joined_group_ids, + "messages": messages + } + except Exception as e: + return {"success": False, "error": str(e)} diff --git a/backend/vendor/camel-oasis/oasis/social_platform/platform_utils.py b/backend/vendor/camel-oasis/oasis/social_platform/platform_utils.py new file mode 100644 index 00000000..333a4bc7 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/platform_utils.py @@ -0,0 +1,262 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import json +from datetime import datetime + +from oasis.social_platform.typing import RecsysType + + +class PlatformUtils: + + def __init__(self, + db, + db_cursor, + start_time, + sandbox_clock, + show_score, + recsys_type, + report_threshold=1): + self.db = db + self.db_cursor = db_cursor + self.start_time = start_time + self.sandbox_clock = sandbox_clock + self.show_score = show_score + self.recsys_type = recsys_type + self.report_threshold = report_threshold + + @staticmethod + def _not_signup_error_message(agent_id): + return { + "success": + False, + "error": (f"Agent {agent_id} has not signed up and does not have " + f"a user id."), + } + + def _execute_db_command(self, command, args=(), commit=False): + self.db_cursor.execute(command, args) + if commit: + self.db.commit() + return self.db_cursor + + def _execute_many_db_command(self, command, args_list, commit=False): + self.db_cursor.executemany(command, args_list) + if commit: + self.db.commit() + return self.db_cursor + + def _check_agent_userid(self, agent_id): + try: + user_query = "SELECT user_id FROM user WHERE agent_id = ?" + results = self._execute_db_command(user_query, (agent_id, )) + # Fetch the first row of the query result + first_row = results.fetchone() + if first_row: + user_id = first_row[0] + return user_id + else: + return None + except Exception as e: + # Log or handle the error as appropriate + print(f"Error querying user_id for agent_id {agent_id}: {e}") + return None + + def _add_comments_to_posts(self, posts_results): + # Initialize the returned posts list + posts = [] + for row in posts_results: + (post_id, user_id, original_post_id, content, quote_content, + created_at, num_likes, num_dislikes, num_shares) = row + post_type_result = self._get_post_type(post_id) + if post_type_result is None: + continue + original_user_id_query = ( + "SELECT user_id FROM post WHERE post_id = ?") + if post_type_result["type"] == "repost": + self.db_cursor.execute(original_user_id_query, + (original_post_id, )) + original_user_id = self.db_cursor.fetchone()[0] + original_post_id = post_id + post_id = post_type_result["root_post_id"] + self.db_cursor.execute( + "SELECT content, quote_content, created_at, num_likes, " + "num_dislikes, num_shares, num_reports FROM post " + "WHERE post_id = ?", (post_id, )) + original_post_result = self.db_cursor.fetchone() + (content, quote_content, created_at, num_likes, num_dislikes, + num_shares, num_reports) = original_post_result + post_content = ( + f"User {user_id} reposted a post from User " + f"{original_user_id}. Repost content: {content}. ") + + elif post_type_result["type"] == "quote": + self.db_cursor.execute(original_user_id_query, + (original_post_id, )) + original_user_id = self.db_cursor.fetchone()[0] + post_content = ( + f"User {user_id} quoted a post from User " + f"{original_user_id}. Quote content: {quote_content}. " + f"Original Content: {content}") + + elif post_type_result["type"] == "common": + post_content = content + # Get num_reports for common posts + self.db_cursor.execute( + "SELECT num_reports FROM post WHERE post_id = ?", + (post_id, )) + num_reports = self.db_cursor.fetchone()[0] + + # For each post, query its corresponding comments + self.db_cursor.execute( + "SELECT comment_id, post_id, user_id, content, created_at, " + "num_likes, num_dislikes FROM comment WHERE post_id = ?", + (post_id, ), + ) + comments_results = self.db_cursor.fetchall() + + # Convert each comment's result into dictionary format + comments = [{ + "comment_id": + comment_id, + "post_id": + post_id, + "user_id": + user_id, + "content": + content, + "created_at": + created_at, + **({ + "score": num_likes - num_dislikes + } if self.show_score else { + "num_likes": num_likes, + "num_dislikes": num_dislikes + }), + } for ( + comment_id, + post_id, + user_id, + content, + created_at, + num_likes, + num_dislikes, + ) in comments_results] + + # Add warning message if the post has been reported + if num_reports >= self.report_threshold: + warning_message = ("[Warning: This post has been reported" + f" {num_reports} times]") + post_content = f"{warning_message}\n{post_content}" + + # Add post information and corresponding comments to the posts list + posts.append({ + "post_id": + post_id + if post_type_result["type"] != "repost" else original_post_id, + "user_id": + user_id, + "content": + post_content, + "created_at": + created_at, + **({ + "score": num_likes - num_dislikes + } if self.show_score else { + "num_likes": num_likes, + "num_dislikes": num_dislikes + }), + "num_shares": + num_shares, + "num_reports": + num_reports, + "comments": + comments, + }) + return posts + + def _record_trace(self, + user_id, + action_type, + action_info, + current_time=None): + r"""If, in addition to the trace, the operation function also records + time in other tables of the database, use the time of entering + the operation function for consistency. + + Pass in current_time to make, for example, the created_at in the post + table exactly the same as the time in the trace table. + + If only the trace table needs to record time, use the entry time into + _record_trace as the time for the trace record. + """ + if self.recsys_type == RecsysType.REDDIT: + current_time = self.sandbox_clock.time_transfer( + datetime.now(), self.start_time) + else: + current_time = self.sandbox_clock.get_time_step() + + trace_insert_query = ( + "INSERT INTO trace (user_id, created_at, action, info) " + "VALUES (?, ?, ?, ?)") + action_info_str = json.dumps(action_info) + self._execute_db_command( + trace_insert_query, + (user_id, current_time, action_type, action_info_str), + commit=True, + ) + + def _check_self_post_rating(self, post_id, user_id): + self_like_check_query = "SELECT user_id FROM post WHERE post_id = ?" + self._execute_db_command(self_like_check_query, (post_id, )) + result = self.db_cursor.fetchone() + if result and result[0] == user_id: + error_message = ("Users are not allowed to like/dislike their own " + "posts.") + return {"success": False, "error": error_message} + else: + return None + + def _check_self_comment_rating(self, comment_id, user_id): + self_like_check_query = ("SELECT user_id FROM comment WHERE " + "comment_id = ?") + self._execute_db_command(self_like_check_query, (comment_id, )) + result = self.db_cursor.fetchone() + if result and result[0] == user_id: + error_message = ("Users are not allowed to like/dislike their " + "own comments.") + return {"success": False, "error": error_message} + else: + return None + + def _get_post_type(self, post_id: int): + query = ( + "SELECT original_post_id, quote_content FROM post WHERE post_id " + "= ?") + self._execute_db_command(query, (post_id, )) + result = self.db_cursor.fetchone() + + if not result: + return None + + original_post_id, quote_content = result + + if original_post_id is None: + # common post without quote or repost + return {"type": "common", "root_post_id": None} + elif quote_content is None: + # post with repost + return {"type": "repost", "root_post_id": original_post_id} + else: + # post with quote + return {"type": "quote", "root_post_id": original_post_id} diff --git a/backend/vendor/camel-oasis/oasis/social_platform/process_recsys_posts.py b/backend/vendor/camel-oasis/oasis/social_platform/process_recsys_posts.py new file mode 100644 index 00000000..6181f94b --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/process_recsys_posts.py @@ -0,0 +1,81 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from typing import List + +import torch +from camel.embeddings import OpenAIEmbedding +from camel.types import EmbeddingModelType +from transformers import AutoModel, AutoTokenizer + + +# Function: Process each batch +@torch.no_grad() +def process_batch(model: AutoModel, tokenizer: AutoTokenizer, + batch_texts: List[str]): + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + inputs = tokenizer(batch_texts, + return_tensors="pt", + padding=True, + truncation=True) + inputs = {key: value.to(device) for key, value in inputs.items()} + outputs = model(**inputs) + return outputs.pooler_output + + +def generate_post_vector(model: AutoModel, tokenizer: AutoTokenizer, texts, + batch_size): + # Loop through all messages + # If the list of messages is too large, process them in batches. + all_outputs = [] + for i in range(0, len(texts), batch_size): + batch_texts = texts[i:i + batch_size] + batch_outputs = process_batch(model, tokenizer, batch_texts) + all_outputs.append(batch_outputs) + all_outputs_tensor = torch.cat(all_outputs, dim=0) # num_posts x dimension + return all_outputs_tensor.cpu() + + +def generate_post_vector_openai(texts: List[str], batch_size: int = 100): + """ + Generate embeddings using OpenAI API + + Args: + texts: List of texts to process + batch_size: Size of each batch + """ + openai_embedding = OpenAIEmbedding( + model_type=EmbeddingModelType.TEXT_EMBEDDING_3_SMALL) + + all_embeddings = [] + for i in range(0, len(texts), batch_size): + batch_texts = texts[i:i + batch_size] + cleaned_texts = [ + text.strip() if text and isinstance(text, str) else "empty" + for text in batch_texts + ] + batch_embeddings = openai_embedding.embed_list(objs=cleaned_texts) + batch_tensor = torch.tensor(batch_embeddings) + all_embeddings.append(batch_tensor) + + return torch.cat(all_embeddings, dim=0) + + +if __name__ == "__main__": + # Input list of strings (assuming there are tens of thousands of messages) + # Here, the same message is repeated 10000 times as an example + texts = ["I'm using TwHIN-BERT! #TwHIN-BERT #NLP"] * 10000 + # Define batch size + batch_size = 100 + all_outputs_tensor = generate_post_vector(texts, batch_size) + print(all_outputs_tensor.shape) diff --git a/backend/vendor/camel-oasis/oasis/social_platform/recsys.py b/backend/vendor/camel-oasis/oasis/social_platform/recsys.py new file mode 100644 index 00000000..9d9429cb --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/recsys.py @@ -0,0 +1,797 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +'''Note that you need to check if it exceeds max_rec_post_len when writing +into rec_matrix''' +import heapq +import logging +import random +import time +from ast import literal_eval +from datetime import datetime +from math import log +from typing import Any, Dict, List + +import numpy as np +import torch +from sentence_transformers import SentenceTransformer +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.metrics.pairwise import cosine_similarity + +from .process_recsys_posts import (generate_post_vector, + generate_post_vector_openai) +from .typing import ActionType, RecsysType + +rec_log = logging.getLogger(name='social.rec') +rec_log.setLevel('DEBUG') + +# Initially set to None, to be assigned once again in the recsys function +model = None +twhin_tokenizer = None +twhin_model = None + +# Create the TF-IDF model +tfidf_vectorizer = TfidfVectorizer() +# Prepare the twhin model +device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + +# All historical tweets and the most recent tweet of each user +user_previous_post_all = {} +user_previous_post = {} +user_profiles = [] +# Get the {post_id: content} dict +t_items = {} +# Get the {uid: follower_count} dict +# It's necessary to ensure that agent registration is sequential, with the +# relationship of user_id=agent_id+1; disorder in registration will cause +# issues here +u_items = {} +# Get the creation times of all tweets, assigning scores based on how recent +# they are +date_score = [] + + +def get_twhin_tokenizer(): + global twhin_tokenizer + if twhin_tokenizer is None: + from transformers import AutoTokenizer + twhin_tokenizer = AutoTokenizer.from_pretrained( + pretrained_model_name_or_path="Twitter/twhin-bert-base", + model_max_length=512) + return twhin_tokenizer + + +def get_twhin_model(device): + global twhin_model + if twhin_model is None: + from transformers import AutoModel + twhin_model = AutoModel.from_pretrained( + pretrained_model_name_or_path="Twitter/twhin-bert-base").to(device) + return twhin_model + + +def load_model(model_name): + try: + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + if model_name == 'paraphrase-MiniLM-L6-v2': + return SentenceTransformer(model_name, + device=device, + cache_folder="./models") + elif model_name == 'Twitter/twhin-bert-base': + twhin_tokenizer = get_twhin_tokenizer() + twhin_model = get_twhin_model(device) + return twhin_tokenizer, twhin_model + else: + raise ValueError(f"Unknown model name: {model_name}") + except Exception as e: + raise Exception(f"Failed to load the model: {model_name}") from e + + +def get_recsys_model(recsys_type: str = None): + if recsys_type == RecsysType.TWITTER.value: + model = load_model('paraphrase-MiniLM-L6-v2') + return model + elif recsys_type == RecsysType.TWHIN.value: + twhin_tokenizer, twhin_model = load_model("Twitter/twhin-bert-base") + models = (twhin_tokenizer, twhin_model) + return models + elif (recsys_type == RecsysType.REDDIT.value + or recsys_type == RecsysType.RANDOM.value): + return None + else: + raise ValueError(f"Unknown recsys type: {recsys_type}") + + +# Move model to GPU if available +device = 'cuda' if torch.cuda.is_available() else 'cpu' +if model is not None: + model.to(device) +else: + pass + + +# Reset global variables +def reset_globals(): + global user_previous_post_all, user_previous_post + global user_profiles, t_items, u_items + global date_score + user_previous_post_all = {} + user_previous_post = {} + user_profiles = [] + t_items = {} + u_items = {} + date_score = [] + + +def rec_sys_random(post_table: List[Dict[str, Any]], rec_matrix: List[List], + max_rec_post_len: int) -> List[List]: + """ + Randomly recommend posts to users. + + Args: + user_table (List[Dict[str, Any]]): List of users. + post_table (List[Dict[str, Any]]): List of posts. + trace_table (List[Dict[str, Any]]): List of user interactions. + rec_matrix (List[List]): Existing recommendation matrix. + max_rec_post_len (int): Maximum number of recommended posts. + + Returns: + List[List]: Updated recommendation matrix. + """ + # Get all post IDs + post_ids = [post['post_id'] for post in post_table] + new_rec_matrix = [] + if len(post_ids) <= max_rec_post_len: + # If the number of posts is less than or equal to the maximum number + # of recommendations, each user gets all post IDs + new_rec_matrix = [post_ids] * len(rec_matrix) + else: + # If the number of posts is greater than the maximum number of + # recommendations, each user randomly gets a specified number of post + # IDs + for _ in range(len(rec_matrix)): + new_rec_matrix.append(random.sample(post_ids, max_rec_post_len)) + + return new_rec_matrix + + +def calculate_hot_score(num_likes: int, num_dislikes: int, + created_at: datetime) -> int: + """ + Compute the hot score for a post. + + Args: + num_likes (int): Number of likes. + num_dislikes (int): Number of dislikes. + created_at (datetime): Creation time of the post. + + Returns: + int: Hot score of the post. + + Reference: + https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9 + """ + s = num_likes - num_dislikes + order = log(max(abs(s), 1), 10) + sign = 1 if s > 0 else -1 if s < 0 else 0 + + # epoch_seconds + epoch = datetime(1970, 1, 1) + td = created_at - epoch + epoch_seconds_result = td.days * 86400 + td.seconds + ( + float(td.microseconds) / 1e6) + + seconds = epoch_seconds_result - 1134028003 + return round(sign * order + seconds / 45000, 7) + + +def get_recommendations( + user_index, + cosine_similarities, + items, + score, + top_n=100, +): + similarities = np.array(cosine_similarities[user_index]) + similarities = similarities * score + top_item_indices = similarities.argsort()[::-1][:top_n] + recommended_items = [(list(items.keys())[i], similarities[i]) + for i in top_item_indices] + return recommended_items + + +def rec_sys_reddit(post_table: List[Dict[str, Any]], rec_matrix: List[List], + max_rec_post_len: int) -> List[List]: + """ + Recommend posts based on Reddit-like hot score. + + Args: + post_table (List[Dict[str, Any]]): List of posts. + rec_matrix (List[List]): Existing recommendation matrix. + max_rec_post_len (int): Maximum number of recommended posts. + + Returns: + List[List]: Updated recommendation matrix. + """ + # Get all post IDs + post_ids = [post['post_id'] for post in post_table] + + if len(post_ids) <= max_rec_post_len: + # If the number of posts is less than or equal to the maximum number + # of recommendations, each user gets all post IDs + new_rec_matrix = [post_ids] * len(rec_matrix) + else: + # The time complexity of this recommendation system is + # O(post_num * log max_rec_post_len) + all_hot_score = [] + for post in post_table: + try: + created_at_dt = datetime.strptime(post['created_at'], + "%Y-%m-%d %H:%M:%S.%f") + except Exception: + created_at_dt = datetime.strptime(post['created_at'], + "%Y-%m-%d %H:%M:%S") + hot_score = calculate_hot_score(post['num_likes'], + post['num_dislikes'], + created_at_dt) + all_hot_score.append((hot_score, post['post_id'])) + # Sort + top_posts = heapq.nlargest(max_rec_post_len, + all_hot_score, + key=lambda x: x[0]) + top_post_ids = [post_id for _, post_id in top_posts] + + # If the number of posts is greater than the maximum number of + # recommendations, each user gets a specified number of post IDs + # randomly + new_rec_matrix = [top_post_ids] * len(rec_matrix) + + return new_rec_matrix + + +def rec_sys_personalized(user_table: List[Dict[str, Any]], + post_table: List[Dict[str, Any]], + trace_table: List[Dict[str, + Any]], rec_matrix: List[List], + max_rec_post_len: int) -> List[List]: + """ + Recommend posts based on personalized similarity scores. + + Args: + user_table (List[Dict[str, Any]]): List of users. + post_table (List[Dict[str, Any]]): List of posts. + trace_table (List[Dict[str, Any]]): List of user interactions. + rec_matrix (List[List]): Existing recommendation matrix. + max_rec_post_len (int): Maximum number of recommended posts. + + Returns: + List[List]: Updated recommendation matrix. + """ + global model + if model is None or isinstance(model, tuple): + model = get_recsys_model(recsys_type="twitter") + + post_ids = [post['post_id'] for post in post_table] + print( + f'Running personalized recommendation for {len(user_table)} users...') + start_time = time.time() + new_rec_matrix = [] + if len(post_ids) <= max_rec_post_len: + # If the number of posts is less than or equal to the maximum + # recommended length, each user gets all post IDs + new_rec_matrix = [post_ids] * len(rec_matrix) + else: + # If the number of posts is greater than the maximum recommended + # length, each user gets personalized post IDs + user_bios = [ + user['bio'] if 'bio' in user and user['bio'] is not None else '' + for user in user_table + ] + post_contents = [post['content'] for post in post_table] + + if model: + user_embeddings = model.encode(user_bios, + convert_to_tensor=True, + device=device) + post_embeddings = model.encode(post_contents, + convert_to_tensor=True, + device=device) + + # Compute dot product similarity + dot_product = torch.matmul(user_embeddings, post_embeddings.T) + + # Compute norm + user_norms = torch.norm(user_embeddings, dim=1) + post_norms = torch.norm(post_embeddings, dim=1) + + # Compute cosine similarity + similarities = dot_product / (user_norms[:, None] * + post_norms[None, :]) + + else: + # Generate random similarities + similarities = torch.rand(len(user_table), len(post_table)) + + # Iterate through each user to generate personalized recommendations. + for user_index, user in enumerate(user_table): + # Filter out posts made by the current user. + filtered_post_indices = [ + i for i, post in enumerate(post_table) + if post['user_id'] != user['user_id'] + ] + + user_similarities = similarities[user_index, filtered_post_indices] + + # Get the corresponding post IDs for the filtered posts. + filtered_post_ids = [ + post_table[i]['post_id'] for i in filtered_post_indices + ] + + # Determine the top posts based on the similarities, limited by + # max_rec_post_len. + _, top_indices = torch.topk(user_similarities, + k=min(max_rec_post_len, + len(filtered_post_ids))) + + top_post_ids = [filtered_post_ids[i] for i in top_indices.tolist()] + + # Append the top post IDs to the new recommendation matrix. + new_rec_matrix.append(top_post_ids) + + end_time = time.time() + print(f'Personalized recommendation time: {end_time - start_time:.6f}s') + return new_rec_matrix + + +def get_like_post_id(user_id, action, trace_table): + """ + Get the post IDs that a user has liked or unliked. + + Args: + user_id (str): ID of the user. + action (str): Type of action (like or unlike). + post_table (list): List of posts. + trace_table (list): List of user interactions. + + Returns: + list: List of post IDs. + """ + # Get post IDs from trace table for the given user and action + trace_post_ids = [ + literal_eval(trace['info'])["post_id"] for trace in trace_table + if (trace['user_id'] == user_id and trace['action'] == action) + ] + """Only take the last 5 liked posts, if not enough, pad with the most + recently liked post. Only take IDs, not content, because calculating + embeddings for all posts again is very time-consuming, especially when the + number of agents is large""" + if len(trace_post_ids) < 5 and len(trace_post_ids) > 0: + trace_post_ids += [trace_post_ids[-1]] * (5 - len(trace_post_ids)) + elif len(trace_post_ids) > 5: + trace_post_ids = trace_post_ids[-5:] + else: + trace_post_ids = [0] + + return trace_post_ids + + +# Calculate the average cosine similarity between liked posts and target posts +def calculate_like_similarity(liked_vectors, target_vectors): + # Calculate the norms of the vectors + liked_norms = np.linalg.norm(liked_vectors, axis=1) + target_norms = np.linalg.norm(target_vectors, axis=1) + # Calculate dot products + dot_products = np.dot(target_vectors, liked_vectors.T) + # Calculate cosine similarities + cosine_similarities = dot_products / np.outer(target_norms, liked_norms) + # Take the average + average_similarities = np.mean(cosine_similarities, axis=1) + + return average_similarities + + +def coarse_filtering(input_list, scale): + """ + Coarse filtering posts and return selected elements with their indices. + """ + if len(input_list) <= scale: + # Return elements and their indices as list of tuples (element, index) + sampled_indices = range(len(input_list)) + return (input_list, sampled_indices) + else: + # Get random sample of scale elements + sampled_indices = random.sample(range(len(input_list)), scale) + sampled_elements = [input_list[idx] for idx in sampled_indices] + # return [(input_list[idx], idx) for idx in sampled_indices] + return (sampled_elements, sampled_indices) + + +def rec_sys_personalized_twh( + user_table: List[Dict[str, Any]], + post_table: List[Dict[str, Any]], + latest_post_count: int, + trace_table: List[Dict[str, Any]], + rec_matrix: List[List], + max_rec_post_len: int, + current_time: int, + # source_post_indexs: List[int], + recall_only: bool = False, + enable_like_score: bool = False, + use_openai_embedding: bool = False) -> List[List]: + global twhin_model, twhin_tokenizer + if twhin_model is None or twhin_tokenizer is None: + twhin_tokenizer, twhin_model = get_recsys_model( + recsys_type="twhin-bert") + # Set some global variables to reduce time consumption + global date_score, t_items, u_items, user_previous_post + global user_previous_post_all, user_profiles + # Get the uid: follower_count dict + # Update only once, unless adding the feature to include new users midway. + if (not u_items) or len(u_items) != len(user_table): + u_items = { + user['user_id']: user["num_followers"] + for user in user_table + } + if not user_previous_post_all or len(user_previous_post_all) != len( + user_table): + # Each user must have a list of historical tweets + user_previous_post_all = { + index: [] + for index in range(len(user_table)) + } + user_previous_post = {index: "" for index in range(len(user_table))} + if not user_profiles or len(user_profiles) != len(user_table): + for user in user_table: + if user['bio'] is None: + user_profiles.append('This user does not have profile') + else: + user_profiles.append(user['bio']) + + if len(t_items) < len(post_table): + for post in post_table[-latest_post_count:]: + # Get the {post_id: content} dict, update only the latest tweets + t_items[post['post_id']] = post['content'] + # Update the user's historical tweets + user_previous_post_all[post['user_id']].append(post['content']) + user_previous_post[post['user_id']] = post['content'] + # Get the creation times of all tweets, assigning scores based on + # how recent they are, note that this algorithm can run for a + # maximum of 90 time steps + date_score.append( + np.log( + (271.8 - (current_time - int(post['created_at']))) / 100)) + + date_score_np = np.array(date_score) + + if enable_like_score: + # Calculate similarity with previously liked content, first gather + # liked post ids from the trace + like_post_ids_all = [] + for user in user_table: + user_id = user['agent_id'] + like_post_ids = get_like_post_id(user_id, + ActionType.LIKE_POST.value, + trace_table) + like_post_ids_all.append(like_post_ids) + scores = date_score_np + new_rec_matrix = [] + if len(post_table) <= max_rec_post_len: + # If the number of tweets is less than or equal to the max + # recommendation count, each user gets all post IDs + tids = [t['post_id'] for t in post_table] + new_rec_matrix = [tids] * (len(rec_matrix)) + + else: + # If the number of tweets is greater than the max recommendation + # count, each user randomly gets personalized post IDs + + # This requires going through all users to update their profiles, + # which is a time-consuming operation + for post_user_index in user_previous_post: + try: + # Directly replacing the profile with the latest tweet will + # cause the recommendation system to repeatedly push other + # reposts to users who have already shared that tweet + # user_profiles[post_user_index] = + # user_previous_post[post_user_index] + # Instead, append the description of the Recent post's content + # to the end of the user char + update_profile = ( + f" # Recent post:{user_previous_post[post_user_index]}") + if user_previous_post[post_user_index] != "": + # If there's no update for the recent post, add this part + if "# Recent post:" not in user_profiles[post_user_index]: + user_profiles[post_user_index] += update_profile + # If the profile has a recent post but it's not the user's + # latest, replace it + elif update_profile not in user_profiles[post_user_index]: + user_profiles[post_user_index] = user_profiles[ + post_user_index].split( + "# Recent post:")[0] + update_profile + except Exception: + print("update previous post failed") + + # coarse filtering 4000 posts due to the memory constraint. + filtered_posts_tuple = coarse_filtering(list(t_items.values()), 4000) + corpus = user_profiles + filtered_posts_tuple[0] + # corpus = user_profiles + list(t_items.values()) + tweet_vector_start_t = time.time() + if use_openai_embedding: + all_post_vector_list = generate_post_vector_openai(corpus, + batch_size=1000) + else: + all_post_vector_list = generate_post_vector(twhin_model, + twhin_tokenizer, + corpus, + batch_size=1000) + tweet_vector_end_t = time.time() + rec_log.info( + f"twhin model cost time: {tweet_vector_end_t-tweet_vector_start_t}" + ) + user_vector = all_post_vector_list[:len(user_profiles)] + posts_vector = all_post_vector_list[len(user_profiles):] + + if enable_like_score: + # Traverse all liked post ids, collecting liked post vectors from + # posts_vector for matrix acceleration calculation + like_posts_vectors = [] + for user_idx, like_post_ids in enumerate(like_post_ids_all): + if len(like_post_ids) != 1: + for like_post_id in like_post_ids: + try: + like_posts_vectors.append( + posts_vector[like_post_id - 1]) + except Exception: + like_posts_vectors.append(user_vector[user_idx]) + else: + like_posts_vectors += [ + user_vector[user_idx] for _ in range(5) + ] + try: + like_posts_vectors = torch.stack(like_posts_vectors).view( + len(user_table), 5, posts_vector.shape[1]) + except Exception: + import pdb # noqa: F811 + pdb.set_trace() + get_similar_start_t = time.time() + cosine_similarities = cosine_similarity(user_vector, posts_vector) + get_similar_end_t = time.time() + rec_log.info(f"get cosine_similarity time: " + f"{get_similar_end_t-get_similar_start_t}") + if enable_like_score: + for user_index, profile in enumerate(user_profiles): + user_like_posts_vector = like_posts_vectors[user_index] + like_scores = calculate_like_similarity( + user_like_posts_vector, posts_vector) + try: + scores = scores + like_scores + except Exception: + import pdb + pdb.set_trace() + + filter_posts_index = filtered_posts_tuple[1] + cosine_similarities = cosine_similarities * scores[filter_posts_index] + cosine_similarities = torch.tensor(cosine_similarities) + value, indices = torch.topk(cosine_similarities, + max_rec_post_len, + dim=1, + largest=True, + sorted=True) + filter_posts_index = torch.tensor(filter_posts_index) + indices = filter_posts_index[indices] + # cosine_similarities = cosine_similarities * scores + # cosine_similarities = torch.tensor(cosine_similarities) + # value, indices = torch.topk(cosine_similarities, + # max_rec_post_len, + # dim=1, + # largest=True, + # sorted=True) + + matrix_list = indices.cpu().numpy() + post_list = list(t_items.keys()) + for rec_ids in matrix_list: + rec_ids = [post_list[i] for i in rec_ids] + new_rec_matrix.append(rec_ids) + + return new_rec_matrix + + +def normalize_similarity_adjustments(post_scores, base_similarity, + like_similarity, dislike_similarity): + """ + Normalize the adjustments to keep them in scale with overall similarities. + + Args: + post_scores (list): List of post scores. + base_similarity (float): Base similarity score. + like_similarity (float): Similarity score for liked posts. + dislike_similarity (float): Similarity score for disliked posts. + + Returns: + float: Adjusted similarity score. + """ + if len(post_scores) == 0: + return base_similarity + + max_score = max(post_scores, key=lambda x: x[1])[1] + min_score = min(post_scores, key=lambda x: x[1])[1] + score_range = max_score - min_score + adjustment = (like_similarity - dislike_similarity) * (score_range / 2) + return base_similarity + adjustment + + +def swap_random_posts(rec_post_ids, post_ids, swap_percent=0.1): + """ + Swap a percentage of recommended posts with random posts. + + Args: + rec_post_ids (list): List of recommended post IDs. + post_ids (list): List of all post IDs. + swap_percent (float): Percentage of posts to swap. + + Returns: + list: Updated list of recommended post IDs. + """ + num_to_swap = int(len(rec_post_ids) * swap_percent) + posts_to_swap = random.sample(post_ids, num_to_swap) + indices_to_replace = random.sample(range(len(rec_post_ids)), num_to_swap) + + for idx, new_post in zip(indices_to_replace, posts_to_swap): + rec_post_ids[idx] = new_post + + return rec_post_ids + + +def get_trace_contents(user_id, action, post_table, trace_table): + """ + Get the contents of posts that a user has interacted with. + + Args: + user_id (str): ID of the user. + action (str): Type of action (like or unlike). + post_table (list): List of posts. + trace_table (list): List of user interactions. + + Returns: + list: List of post contents. + """ + # Get post IDs from trace table for the given user and action + trace_post_ids = [ + trace['post_id'] for trace in trace_table + if (trace['user_id'] == user_id and trace['action'] == action) + ] + # Fetch post contents from post table where post IDs match those in the + # trace + trace_contents = [ + post['content'] for post in post_table + if post['post_id'] in trace_post_ids + ] + return trace_contents + + +def rec_sys_personalized_with_trace( + user_table: List[Dict[str, Any]], + post_table: List[Dict[str, Any]], + trace_table: List[Dict[str, Any]], + rec_matrix: List[List], + max_rec_post_len: int, + swap_rate: float = 0.1, +) -> List[List]: + """ + This version: + 1. If the number of posts is less than or equal to the maximum + recommended length, each user gets all post IDs + + 2. Otherwise: + - For each user, get a like-trace pool and dislike-trace pool from the + trace table + - For each user, calculate the similarity between the user's bio and + the post text + - Use the trace table to adjust the similarity score + - Swap 10% of the recommended posts with the random posts + + Personalized recommendation system that uses user interaction traces. + + Args: + user_table (List[Dict[str, Any]]): List of users. + post_table (List[Dict[str, Any]]): List of posts. + trace_table (List[Dict[str, Any]]): List of user interactions. + rec_matrix (List[List]): Existing recommendation matrix. + max_rec_post_len (int): Maximum number of recommended posts. + swap_rate (float): Percentage of posts to swap for diversity. + + Returns: + List[List]: Updated recommendation matrix. + """ + + start_time = time.time() + + new_rec_matrix = [] + post_ids = [post['post_id'] for post in post_table] + if len(post_ids) <= max_rec_post_len: + new_rec_matrix = [post_ids] * (len(rec_matrix) - 1) + else: + for idx in range(1, len(rec_matrix)): + user_id = user_table[idx - 1]['user_id'] + user_bio = user_table[idx - 1]['bio'] + # filter out posts that belong to the user + available_post_contents = [(post['post_id'], post['content']) + for post in post_table + if post['user_id'] != user_id] + + # filter out like-trace and dislike-trace + like_trace_contents = get_trace_contents( + user_id, ActionType.LIKE_POST.value, post_table, trace_table) + dislike_trace_contents = get_trace_contents( + user_id, ActionType.UNLIKE_POST.value, post_table, trace_table) + # calculate similarity between user bio and post text + post_scores = [] + for post_id, post_content in available_post_contents: + if model is not None: + user_embedding = model.encode(user_bio) + post_embedding = model.encode(post_content) + base_similarity = np.dot( + user_embedding, + post_embedding) / (np.linalg.norm(user_embedding) * + np.linalg.norm(post_embedding)) + post_scores.append((post_id, base_similarity)) + else: + post_scores.append((post_id, random.random())) + + new_post_scores = [] + # adjust similarity based on like and dislike traces + for _post_id, _base_similarity in post_scores: + _post_content = post_table[post_ids.index(_post_id)]['content'] + like_similarity = sum( + np.dot(model.encode(_post_content), model.encode(like)) / + (np.linalg.norm(model.encode(_post_content)) * + np.linalg.norm(model.encode(like))) + for like in like_trace_contents) / len( + like_trace_contents) if like_trace_contents else 0 + dislike_similarity = sum( + np.dot(model.encode(_post_content), model.encode(dislike)) + / (np.linalg.norm(model.encode(_post_content)) * + np.linalg.norm(model.encode(dislike))) + for dislike in dislike_trace_contents) / len( + dislike_trace_contents + ) if dislike_trace_contents else 0 + + # Normalize and apply adjustments + adjusted_similarity = normalize_similarity_adjustments( + post_scores, _base_similarity, like_similarity, + dislike_similarity) + new_post_scores.append((_post_id, adjusted_similarity)) + + # sort posts by similarity + new_post_scores.sort(key=lambda x: x[1], reverse=True) + # extract post ids + rec_post_ids = [ + post_id for post_id, _ in new_post_scores[:max_rec_post_len] + ] + + if swap_rate > 0: + # swap the recommended posts with random posts + swap_free_ids = [ + post_id for post_id in post_ids + if post_id not in rec_post_ids and post_id not in [ + trace['post_id'] + for trace in trace_table if trace['user_id'] + ] + ] + rec_post_ids = swap_random_posts(rec_post_ids, swap_free_ids, + swap_rate) + + new_rec_matrix.append(rec_post_ids) + end_time = time.time() + print(f'Personalized recommendation time: {end_time - start_time:.6f}s') + return new_rec_matrix diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/chat_group.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/chat_group.sql new file mode 100644 index 00000000..1d00a494 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/chat_group.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS `chat_group` ( + group_id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/comment.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/comment.sql new file mode 100644 index 00000000..71c70d46 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/comment.sql @@ -0,0 +1,12 @@ +-- This is the schema definition for the comment table +CREATE TABLE comment ( + comment_id INTEGER PRIMARY KEY AUTOINCREMENT, + post_id INTEGER, + user_id INTEGER, + content TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + num_likes INTEGER DEFAULT 0, + num_dislikes INTEGER DEFAULT 0, + FOREIGN KEY(post_id) REFERENCES post(post_id), + FOREIGN KEY(user_id) REFERENCES user(user_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/comment_dislike.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/comment_dislike.sql new file mode 100644 index 00000000..b118d3a4 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/comment_dislike.sql @@ -0,0 +1,9 @@ +-- This is the schema definition for the comment_dislike table +CREATE TABLE comment_dislike ( + comment_dislike_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + comment_id INTEGER, + created_at DATETIME, + FOREIGN KEY(user_id) REFERENCES user(user_id), + FOREIGN KEY(comment_id) REFERENCES comment(comment_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/comment_like.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/comment_like.sql new file mode 100644 index 00000000..d60c0a86 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/comment_like.sql @@ -0,0 +1,9 @@ +-- This is the schema definition for the comment_like table +CREATE TABLE comment_like ( + comment_like_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + comment_id INTEGER, + created_at DATETIME, + FOREIGN KEY(user_id) REFERENCES user(user_id), + FOREIGN KEY(comment_id) REFERENCES comment(comment_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/dislike.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/dislike.sql new file mode 100644 index 00000000..120df591 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/dislike.sql @@ -0,0 +1,9 @@ +-- This is the schema definition for the dislike table +CREATE TABLE dislike ( + dislike_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + post_id INTEGER, + created_at DATETIME, + FOREIGN KEY(user_id) REFERENCES user(user_id), + FOREIGN KEY(post_id) REFERENCES tweet(post_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/follow.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/follow.sql new file mode 100644 index 00000000..c747e82e --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/follow.sql @@ -0,0 +1,9 @@ +-- This is the schema definition for the follow table +CREATE TABLE follow ( + follow_id INTEGER PRIMARY KEY AUTOINCREMENT, + follower_id INTEGER, + followee_id INTEGER, + created_at DATETIME, + FOREIGN KEY(follower_id) REFERENCES user(user_id), + FOREIGN KEY(followee_id) REFERENCES user(user_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/group_member.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/group_member.sql new file mode 100644 index 00000000..3411cdb4 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/group_member.sql @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS group_members ( + group_id INTEGER NOT NULL, + agent_id INTEGER NOT NULL, + joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (group_id, agent_id), + FOREIGN KEY (group_id) REFERENCES chat_group(group_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/group_message.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/group_message.sql new file mode 100644 index 00000000..6b4eefdf --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/group_message.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS group_messages ( + message_id INTEGER PRIMARY KEY AUTOINCREMENT, + group_id INTEGER NOT NULL, + sender_id INTEGER NOT NULL, + content TEXT NOT NULL, + sent_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (group_id) REFERENCES chat_group(group_id), + FOREIGN KEY (sender_id) REFERENCES user(agent_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/like.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/like.sql new file mode 100644 index 00000000..82c74076 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/like.sql @@ -0,0 +1,9 @@ +-- This is the schema definition for the like table +CREATE TABLE like ( + like_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + post_id INTEGER, + created_at DATETIME, + FOREIGN KEY(user_id) REFERENCES user(user_id), + FOREIGN KEY(post_id) REFERENCES tweet(post_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/mute.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/mute.sql new file mode 100644 index 00000000..cefa72c4 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/mute.sql @@ -0,0 +1,9 @@ +-- This is the schema definition for the mute table +CREATE TABLE mute ( + mute_id INTEGER PRIMARY KEY AUTOINCREMENT, + muter_id INTEGER, + mutee_id INTEGER, + created_at DATETIME, + FOREIGN KEY(muter_id) REFERENCES user(user_id), + FOREIGN KEY(mutee_id) REFERENCES user(user_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/post.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/post.sql new file mode 100644 index 00000000..d668299b --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/post.sql @@ -0,0 +1,16 @@ +-- This is the schema definition for the post table +-- Add Images, location etc.? +CREATE TABLE post ( + post_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + original_post_id INTEGER, -- NULL if this is an original post + content TEXT DEFAULT '', -- DEFAULT '' for initial posts + quote_content TEXT, -- NULL if this is an original post or a repost + created_at DATETIME, + num_likes INTEGER DEFAULT 0, + num_dislikes INTEGER DEFAULT 0, + num_shares INTEGER DEFAULT 0, -- num_shares = num_reposts + num_quotes + num_reports INTEGER DEFAULT 0, + FOREIGN KEY(user_id) REFERENCES user(user_id), + FOREIGN KEY(original_post_id) REFERENCES post(post_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/product.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/product.sql new file mode 100644 index 00000000..ad8060b8 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/product.sql @@ -0,0 +1,6 @@ +-- This is the schema definition for the product table +CREATE TABLE product ( + product_id INTEGER PRIMARY KEY, + product_name TEXT, + sales INTEGER DEFAULT 0 +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/rec.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/rec.sql new file mode 100644 index 00000000..89b6a694 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/rec.sql @@ -0,0 +1,8 @@ +-- This is the schema definition for the rec table +CREATE TABLE rec ( + user_id INTEGER, + post_id INTEGER, + PRIMARY KEY(user_id, post_id), + FOREIGN KEY(user_id) REFERENCES user(user_id) + FOREIGN KEY(post_id) REFERENCES tweet(post_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/report.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/report.sql new file mode 100644 index 00000000..232c2e0a --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/report.sql @@ -0,0 +1,10 @@ +-- This is the schema definition for the report table +CREATE TABLE report ( + report_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER, + post_id INTEGER, + report_reason TEXT, + created_at DATETIME, + FOREIGN KEY(user_id) REFERENCES user(user_id), + FOREIGN KEY(post_id) REFERENCES post(post_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/trace.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/trace.sql new file mode 100644 index 00000000..13b47afa --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/trace.sql @@ -0,0 +1,9 @@ +-- This is the schema definition for the trace table +CREATE TABLE trace ( + user_id INTEGER, + created_at DATETIME, + action TEXT, + info TEXT, + PRIMARY KEY(user_id, created_at, action, info), + FOREIGN KEY(user_id) REFERENCES user(user_id) +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/schema/user.sql b/backend/vendor/camel-oasis/oasis/social_platform/schema/user.sql new file mode 100644 index 00000000..a5b4d0d6 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/schema/user.sql @@ -0,0 +1,11 @@ +-- This is the schema definition for the user table +CREATE TABLE user ( + user_id INTEGER PRIMARY KEY AUTOINCREMENT, + agent_id INTEGER, + user_name TEXT, + name TEXT, + bio TEXT, + created_at DATETIME, + num_followings INTEGER DEFAULT 0, + num_followers INTEGER DEFAULT 0 +); diff --git a/backend/vendor/camel-oasis/oasis/social_platform/typing.py b/backend/vendor/camel-oasis/oasis/social_platform/typing.py new file mode 100644 index 00000000..0a5700e8 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/social_platform/typing.py @@ -0,0 +1,90 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +from enum import Enum + + +class ActionType(Enum): + EXIT = "exit" + REFRESH = "refresh" + SEARCH_USER = "search_user" + SEARCH_POSTS = "search_posts" + CREATE_POST = "create_post" + LIKE_POST = "like_post" + UNLIKE_POST = "unlike_post" + DISLIKE_POST = "dislike_post" + UNDO_DISLIKE_POST = "undo_dislike_post" + REPORT_POST = "report_post" + FOLLOW = "follow" + UNFOLLOW = "unfollow" + MUTE = "mute" + UNMUTE = "unmute" + TREND = "trend" + SIGNUP = "sign_up" + REPOST = "repost" + QUOTE_POST = "quote_post" + UPDATE_REC_TABLE = "update_rec_table" + CREATE_COMMENT = "create_comment" + LIKE_COMMENT = "like_comment" + UNLIKE_COMMENT = "unlike_comment" + DISLIKE_COMMENT = "dislike_comment" + UNDO_DISLIKE_COMMENT = "undo_dislike_comment" + DO_NOTHING = "do_nothing" + PURCHASE_PRODUCT = "purchase_product" + INTERVIEW = "interview" + JOIN_GROUP = "join_group" + LEAVE_GROUP = "leave_group" + SEND_TO_GROUP = "send_to_group" + CREATE_GROUP = "create_group" + LISTEN_FROM_GROUP = "listen_from_group" + + @classmethod + def get_default_twitter_actions(cls): + return [ + cls.CREATE_POST, + cls.LIKE_POST, + cls.REPOST, + cls.FOLLOW, + cls.DO_NOTHING, + cls.QUOTE_POST, + ] + + @classmethod + def get_default_reddit_actions(cls): + return [ + cls.LIKE_POST, + cls.DISLIKE_POST, + cls.CREATE_POST, + cls.CREATE_COMMENT, + cls.LIKE_COMMENT, + cls.DISLIKE_COMMENT, + cls.SEARCH_POSTS, + cls.SEARCH_USER, + cls.TREND, + cls.REFRESH, + cls.DO_NOTHING, + cls.FOLLOW, + cls.MUTE, + ] + + +class RecsysType(Enum): + TWITTER = "twitter" + TWHIN = "twhin-bert" + REDDIT = "reddit" + RANDOM = "random" + + +class DefaultPlatformType(Enum): + TWITTER = "twitter" + REDDIT = "reddit" diff --git a/backend/vendor/camel-oasis/oasis/testing/__init__.py b/backend/vendor/camel-oasis/oasis/testing/__init__.py new file mode 100644 index 00000000..d963f8cc --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/testing/__init__.py @@ -0,0 +1,13 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== diff --git a/backend/vendor/camel-oasis/oasis/testing/show_db.py b/backend/vendor/camel-oasis/oasis/testing/show_db.py new file mode 100644 index 00000000..986102d9 --- /dev/null +++ b/backend/vendor/camel-oasis/oasis/testing/show_db.py @@ -0,0 +1,64 @@ +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +# Licensed under the Apache License, Version 2.0 (the “License”); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an “AS IS” BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# =========== Copyright 2023 @ CAMEL-AI.org. All Rights Reserved. =========== +import logging +import sqlite3 +from datetime import datetime + +table_log = logging.getLogger(name="table") +table_log.setLevel("DEBUG") +now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") # Modify here +file_handler = logging.FileHandler(f"./log/table-{str(now)}.log", + encoding="utf-8") +file_handler.setLevel("DEBUG") +file_handler.setFormatter(logging.Formatter("%(message)s")) +table_log.addHandler(file_handler) +stream_handler = logging.StreamHandler() +stream_handler.setLevel("DEBUG") +stream_handler.setFormatter(logging.Formatter("%(message)s")) +table_log.addHandler(stream_handler) + + +def print_db_contents(db_file): + # Connect to the SQLite database + conn = sqlite3.connect(db_file) + cursor = conn.cursor() + + # Retrieve and print all table names + cursor.execute("SELECT name FROM sqlite_master WHERE type='table';") + tables = cursor.fetchall() + # print("Tables:", [table[0] for table in tables]) + table_log.info("Tables:" + " ".join([str(table[0]) for table in tables])) + + for table_name in tables: + # print(f"\nTable: {table_name[0]}") + table_log.info(f"\nTable: {table_name[0]}") + # Print table structure + cursor.execute(f"PRAGMA table_info({table_name[0]})") + columns = cursor.fetchall() + # print("Columns:") + table_log.info("Columns:") + for col in columns: + # print(f" {col[1]} ({col[2]})") + table_log.info(f" {col[1]} ({col[2]})") + + # Print table contents + cursor.execute(f"SELECT * FROM {table_name[0]}") + rows = cursor.fetchall() + # print("Contents:") + table_log.info("Contents:") + for row in rows: + # print(" ", row) + table_log.info(" " + ", ".join(str(item) for item in row)) + # Close the connection + conn.close() diff --git a/backend/vendor/camel-oasis/pyproject.toml b/backend/vendor/camel-oasis/pyproject.toml new file mode 100644 index 00000000..9d56db30 --- /dev/null +++ b/backend/vendor/camel-oasis/pyproject.toml @@ -0,0 +1,41 @@ +[project] +name = "camel-oasis" +version = "0.2.5.post1" +description = "Open Agents Social Interaction Simulations on a Large Scale (vendored fork: relaxed neo4j pin to coexist with graphiti-core[falkordb])" +authors = [{ name = "CAMEL-AI.org" }] +readme = "README.md" +license = { text = "Apache-2.0" } +keywords = ["multi-agent-systems", "social-simulations"] +requires-python = ">=3.11,<3.13" + +dependencies = [ + "pandas==2.2.2", + "igraph==0.11.6", + "cairocffi==1.7.1", + "pillow==10.3.0", + "unstructured==0.13.7", + "sentence-transformers==3.0.0", + "prance==23.6.21.0", + "openapi-spec-validator==0.7.1", + "slack_sdk==3.31.0", + # Relaxed from ==5.23.0 to coexist with graphiti-core[falkordb]'s + # neo4j>=5.26.0 requirement. The MiroFish code paths (run_*_simulation.py + # + run_parallel_simulation.py) never pass a neo4j_config, so AgentGraph + # is constructed in-memory-only and the neo4j driver is never invoked. + "neo4j>=5.23.0", + "camel-ai==0.2.78", + "requests_oauthlib==2.0.0", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["oasis"] + +# Required because the parent project (MiroFish) declares this package as +# a direct-reference local path dep ("camel-oasis @ file:///app/backend/vendor/camel-oasis"). +# Without this, hatchling refuses to build when uv is constructing the lock entry. +[tool.hatch.metadata] +allow-direct-references = true diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 00000000..138dc5b7 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,81 @@ +# MiroFish deploy overlay — agent.profikid.nl +# +# Project conventions (matches hermes, snake, wiki siblings): +# /docker//docker-compose.yml +# COMPOSE_PROJECT_NAME= +# TRAEFIK_HOST=agent.profikid.nl +# +# Single port (3000) exposed to Traefik; nginx inside the container +# serves the built frontend and reverse-proxies /api/* + /health to Flask. +# +# Memory graph: FalkorDB (in-memory graph store) on a shared network. + +services: + falkordb: + image: falkordb/falkordb:latest + container_name: mirofish-falkordb + restart: unless-stopped + expose: + - "6379" + volumes: + - falkordb-data:/data + healthcheck: + test: ["CMD-SHELL", "redis-cli ping | grep -q PONG || exit 1"] + interval: 15s + timeout: 3s + retries: 5 + start_period: 20s + networks: + - mirofish_net + + mirofish: + build: + context: .. + dockerfile: Dockerfile + image: mirofish:latest + container_name: mirofish + restart: unless-stopped + depends_on: + falkordb: + condition: service_healthy + env_file: + - secrets.env + environment: + FALKORDB_HOST: falkordb + FALKORDB_PORT: "6379" + expose: + - "3000" + labels: + - traefik.enable=true + - traefik.http.routers.${COMPOSE_PROJECT_NAME:-mirofish}.rule=Host(`${COMPOSE_PROJECT_NAME:-mirofish}.${TRAEFIK_HOST:-agent.profikid.nl}`) + - traefik.http.routers.${COMPOSE_PROJECT_NAME:-mirofish}.entrypoints=websecure + - traefik.http.routers.${COMPOSE_PROJECT_NAME:-mirofish}.tls.certresolver=letsencrypt + - traefik.http.services.${COMPOSE_PROJECT_NAME:-mirofish}.loadbalancer.server.port=3000 + # HTTP -> HTTPS redirect + - traefik.http.routers.${COMPOSE_PROJECT_NAME:-mirofish}-http.rule=Host(`${COMPOSE_PROJECT_NAME:-mirofish}.${TRAEFIK_HOST:-agent.profikid.nl}`) + - traefik.http.routers.${COMPOSE_PROJECT_NAME:-mirofish}-http.entrypoints=web + - traefik.http.routers.${COMPOSE_PROJECT_NAME:-mirofish}-http.middlewares=redirect-to-https + - traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https + - traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true + volumes: + - uploads:/app/backend/uploads + - flask-logs:/app/logs + - embedding-model:/root/.cache/huggingface # cache the sentence-transformers model + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:3000/health"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 90s + networks: + - mirofish_net + +volumes: + uploads: + flask-logs: + falkordb-data: + embedding-model: + +networks: + mirofish_net: + name: mirofish_net diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..2c9cd263 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,65 @@ +worker_processes 1; +pid /run/nginx.pid; +error_log /var/log/nginx/error.log warn; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + client_max_body_size 100m; # upload endpoint accepts up to 50MB per file, several at once + access_log /var/log/nginx/access.log; + + upstream flask_backend { + server 127.0.0.1:5001; + keepalive 8; + } + + server { + listen 3000 default_server; + server_name _; + + root /app/frontend/dist; + index index.html; + + # Long timeouts: backend's ontology/profile/report generation can take minutes + proxy_connect_timeout 600s; + proxy_send_timeout 600s; + proxy_read_timeout 600s; + send_timeout 600s; + + # Reverse-proxy /api/* and /health to Flask + location /api/ { + proxy_pass http://flask_backend/api/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; # important for SSE / streamed responses + } + + location = /health { + proxy_pass http://flask_backend/health; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # Vite emits /assets/. for JS/CSS; let nginx serve with proper mime + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # SPA fallback: send everything else to index.html + location / { + try_files $uri $uri/ /index.html; + } + } +} diff --git a/start.sh b/start.sh new file mode 100644 index 00000000..938dd2eb --- /dev/null +++ b/start.sh @@ -0,0 +1,38 @@ +#!/bin/sh +# Start the Flask backend in the background, then run nginx in the foreground. +# Both share the same container. + +set -e + +# Tell Flask to bind to localhost only (nginx fronts it) +export FLASK_HOST=127.0.0.1 +export FLASK_PORT=5001 +# Disable Werkzeug reloader / debug for a single-process prod-like run +export FLASK_DEBUG=False + +echo "[start.sh] launching Flask backend on 127.0.0.1:5001 ..." +cd /app/backend +uv run python run.py > /app/logs/flask.log 2>&1 & +FLASK_PID=$! + +# Wait for Flask to be ready (max 60s) +echo "[start.sh] waiting for Flask to respond on /health ..." +i=0 +while [ $i -lt 60 ]; do + if curl -sSf -m 2 http://127.0.0.1:5001/health >/dev/null 2>&1; then + echo "[start.sh] Flask is up (pid $FLASK_PID)." + break + fi + i=$((i+1)) + sleep 1 +done +if [ $i -ge 60 ]; then + echo "[start.sh] WARN: Flask did not become ready in 60s; check /app/logs/flask.log" +fi + +# Make sure nginx can write its pid file +mkdir -p /run +chown -R www-data:www-data /var/lib/nginx /var/log/nginx /run 2>/dev/null || true + +echo "[start.sh] launching nginx on 0.0.0.0:3000 ..." +exec nginx -g "daemon off;"