From 5311c99d69ef1cae004240043cabe235a17130d0 Mon Sep 17 00:00:00 2001 From: caoxiaozhu Date: Wed, 24 Jun 2026 21:58:35 +0800 Subject: [PATCH] =?UTF-8?q?refactor(server):=20steward=20=E5=86=B3?= =?UTF-8?q?=E7=AD=96=E9=93=BE=E8=B7=AF=E6=94=B9=E7=94=A8=20LangGraph=20?= =?UTF-8?q?=E7=BC=96=E6=8E=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 StewardGraphPlannerService,用 LangGraph 状态图编排意图识别→流程判断→模型/规则分支→兜底,替代原 planner 内线性调用 - 新增 StewardGraphRuntimeService 编排运行时决策与槽位决策;StewardActionContracts/Executor 统一动作合约与执行 - steward_intent_agent/application_fact_resolver/runtime_chat 适配图执行器,config 暴露图相关开关 - pyproject/uv.lock 新增 langgraph 依赖 - 新增 graph_planner/graph_runtime/action_executor 测试,更新 intent_agent/planner/fact_resolver/runtime_chat/reimbursement 测试 --- server/pyproject.toml | 1 + server/src/app/api/v1/endpoints/steward.py | 72 +- server/src/app/core/config.py | 1 + server/src/app/schemas/steward.py | 56 +- .../app/services/application_fact_resolver.py | 87 ++- server/src/app/services/runtime_chat.py | 10 +- .../app/services/steward_action_contracts.py | 224 ++++++ .../app/services/steward_action_executor.py | 570 +++++++++++++++ .../services/steward_graph_action_runtime.py | 204 ++++++ .../src/app/services/steward_graph_planner.py | 220 ++++++ .../src/app/services/steward_graph_runtime.py | 214 ++++++ .../src/app/services/steward_intent_agent.py | 12 +- .../services/steward_model_plan_builder.py | 18 + server/src/app/services/steward_planner.py | 52 +- .../services/steward_planner_extraction.py | 11 +- .../app/services/steward_planner_fallback.py | 28 +- .../tests/test_application_fact_resolver.py | 11 + server/tests/test_reimbursement_endpoints.py | 4 +- server/tests/test_runtime_chat_service.py | 92 +++ server/tests/test_steward_action_executor.py | 455 ++++++++++++ server/tests/test_steward_graph_planner.py | 234 +++++++ server/tests/test_steward_graph_runtime.py | 268 +++++++ server/tests/test_steward_intent_agent.py | 40 ++ server/tests/test_steward_planner.py | 137 +++- server/uv.lock | 663 ++++++++++++++++-- 25 files changed, 3580 insertions(+), 104 deletions(-) create mode 100644 server/src/app/services/steward_action_contracts.py create mode 100644 server/src/app/services/steward_action_executor.py create mode 100644 server/src/app/services/steward_graph_action_runtime.py create mode 100644 server/src/app/services/steward_graph_planner.py create mode 100644 server/src/app/services/steward_graph_runtime.py create mode 100644 server/tests/test_steward_action_executor.py create mode 100644 server/tests/test_steward_graph_planner.py create mode 100644 server/tests/test_steward_graph_runtime.py diff --git a/server/pyproject.toml b/server/pyproject.toml index a3864bc..d9a3112 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "email-validator>=2.2.0,<3.0.0", "python-multipart>=0.0.20,<1.0.0", "jieba>=0.42.1,<0.43.0", + "langgraph>=1.2.0,<2.0.0", "openpyxl>=3.1.5,<4.0.0", "lightrag-hku>=1.4.16,<1.5.0", "qdrant-client>=1.18.0,<2.0.0", diff --git a/server/src/app/api/v1/endpoints/steward.py b/server/src/app/api/v1/endpoints/steward.py index 4092a06..a877891 100644 --- a/server/src/app/api/v1/endpoints/steward.py +++ b/server/src/app/api/v1/endpoints/steward.py @@ -10,10 +10,13 @@ from fastapi.responses import StreamingResponse from sqlalchemy import select from sqlalchemy.orm import Session -from app.api.deps import get_db +from app.api.deps import CurrentUserContext, get_current_user, get_db +from app.core.config import get_settings from app.models.financial_record import ExpenseClaim from app.schemas.common import ErrorResponse from app.schemas.steward import ( + StewardActionExecuteRequest, + StewardActionExecuteResponse, StewardPlanRequest, StewardPlanResponse, StewardRuntimeDecisionRequest, @@ -27,6 +30,9 @@ from app.services.expense_claim_draft_flow import APPROVED_APPLICATION_LINK_STAT from app.services.expense_claims import ExpenseClaimService from app.services.runtime_chat import RuntimeChatService from app.services.steward_flow_state import StewardFlowStateService +from app.services.steward_graph_action_runtime import StewardGraphActionRuntime +from app.services.steward_graph_planner import StewardGraphPlannerService +from app.services.steward_graph_runtime import StewardGraphRuntime from app.services.steward_intent_agent import StewardIntentAgent from app.services.steward_off_topic_agent import StewardOffTopicAgent from app.services.steward_planner import StewardPlannerService @@ -35,6 +41,8 @@ from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent router = APIRouter(prefix="/steward") DbSession = Annotated[Session, Depends(get_db)] +CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)] +StewardPlannerLike = StewardPlannerService | StewardGraphPlannerService @router.post( @@ -69,7 +77,7 @@ def create_steward_slot_decision( payload: StewardSlotDecisionRequest, db: DbSession, ) -> StewardSlotDecisionResponse: - return StewardSlotDecisionAgent(RuntimeChatService(db)).decide(payload) + return _decide_steward_slot(payload, RuntimeChatService(db)) @router.post( @@ -83,10 +91,27 @@ def create_steward_runtime_decision( db: DbSession, ) -> StewardRuntimeDecisionResponse: hydrated_payload = _hydrate_runtime_decision_payload(db, payload) - decision = StewardRuntimeDecisionAgent(RuntimeChatService(db)).decide(hydrated_payload) + decision = _decide_steward_runtime(hydrated_payload, RuntimeChatService(db)) return _attach_runtime_conversation_state(db, hydrated_payload, decision) +@router.post( + "/actions/execute", + response_model=StewardActionExecuteResponse, + summary="执行小财管家白名单动作", + description=( + "按 LangGraph 规划出的 action step 执行确定性业务动作;" + "当 action 未知、缺字段、缺确认或预检查未通过时直接阻断并返回结构化兜底结果。" + ), +) +def execute_steward_action( + payload: StewardActionExecuteRequest, + db: DbSession, + current_user: CurrentUser, +) -> StewardActionExecuteResponse: + return StewardGraphActionRuntime(db).execute(payload, current_user) + + @router.post( "/plans/stream", summary="流式生成小财管家任务计划", @@ -101,7 +126,7 @@ async def stream_steward_plan(payload: StewardPlanRequest, db: DbSession) -> Str async def _iter_steward_plan_events( payload: StewardPlanRequest, - planner: StewardPlannerService, + planner: StewardPlannerLike, db: Session, ) -> AsyncIterator[str]: yield _encode_stream_event( @@ -135,18 +160,53 @@ def _encode_stream_event(event: str, data: dict[str, Any]) -> str: return json.dumps({"event": event, "data": data}, ensure_ascii=False) + "\n" -def _build_steward_planner(db: Session) -> StewardPlannerService: +def _build_steward_planner(db: Session) -> StewardPlannerLike: runtime_chat = RuntimeChatService(db) + if get_settings().steward_agent_runtime.strip().lower() == "langgraph": + return StewardGraphPlannerService( + intent_agent=StewardIntentAgent(runtime_chat), + off_topic_agent=StewardOffTopicAgent(runtime_chat), + ) return StewardPlannerService( intent_agent=StewardIntentAgent(runtime_chat), off_topic_agent=StewardOffTopicAgent(runtime_chat), ) +def _decide_steward_slot( + payload: StewardSlotDecisionRequest, + runtime_chat: Any, +) -> StewardSlotDecisionResponse: + legacy_agent = StewardSlotDecisionAgent(runtime_chat) + if not _should_use_langgraph_runtime(): + return legacy_agent.decide(payload) + try: + return StewardGraphRuntime(runtime_chat).decide_slot(payload) + except Exception: + return legacy_agent.decide(payload) + + +def _decide_steward_runtime( + payload: StewardRuntimeDecisionRequest, + runtime_chat: Any, +) -> StewardRuntimeDecisionResponse: + legacy_agent = StewardRuntimeDecisionAgent(runtime_chat) + if not _should_use_langgraph_runtime(): + return legacy_agent.decide(payload) + try: + return StewardGraphRuntime(runtime_chat).decide_runtime(payload) + except Exception: + return legacy_agent.decide(payload) + + +def _should_use_langgraph_runtime() -> bool: + return get_settings().steward_agent_runtime.strip().lower() == "langgraph" + + def _hydrate_required_application_gate( db: Session, payload: StewardPlanRequest, - planner: StewardPlannerService, + planner: StewardPlannerLike, ) -> StewardPlanRequest: context_json = dict(payload.context_json or {}) required_gate = context_json.get("required_application_gate") diff --git a/server/src/app/core/config.py b/server/src/app/core/config.py index 7c8dd82..f690b7c 100644 --- a/server/src/app/core/config.py +++ b/server/src/app/core/config.py @@ -73,6 +73,7 @@ class Settings(BaseSettings): onlyoffice_backend_url: str = Field(default="", alias="ONLYOFFICE_BACKEND_URL") onlyoffice_jwt_secret: str = Field(default="", alias="ONLYOFFICE_JWT_SECRET") hermes_agent_shared_token: str = Field(default="", alias="HERMES_AGENT_SHARED_TOKEN") + steward_agent_runtime: str = Field(default="langgraph", alias="STEWARD_AGENT_RUNTIME") log_level: str = Field(default="INFO", alias="LOG_LEVEL") log_dir: str = Field(default="logs", alias="LOG_DIR") diff --git a/server/src/app/schemas/steward.py b/server/src/app/schemas/steward.py index c4ae0b5..07c7168 100644 --- a/server/src/app/schemas/steward.py +++ b/server/src/app/schemas/steward.py @@ -4,11 +4,11 @@ from typing import Any, Literal from pydantic import BaseModel, Field - StewardTaskType = Literal["expense_application", "reimbursement"] StewardAssignedAgent = Literal["application_assistant", "reimbursement_assistant"] StewardPlanningSource = Literal["llm_function_call", "rule_fallback"] StewardPlanNextAction = Literal["confirm_flow", "confirm_task", "delegate_task", "none"] +StewardRequestedAction = Literal["preview", "save_draft", "submit"] StewardSlotDecisionSource = Literal["llm_function_call", "rule_fallback"] StewardSlotNextAction = Literal["ask_user", "render_preview"] StewardRuntimeDecisionSource = Literal["llm_function_call", "rule_fallback"] @@ -22,6 +22,22 @@ StewardRuntimeNextAction = Literal[ "cancel_current_action", "no_op", ] +StewardActionType = Literal[ + "detect_intent", + "fill_application_fields", + "build_application_preview", + "fill_reimbursement_fields", + "build_reimbursement_preview", + "validate_required_fields", + "run_duplicate_precheck", + "save_application_draft", + "submit_application", + "link_existing_application", + "create_reimbursement_draft", + "associate_attachments", +] +StewardActionStatus = Literal["completed", "planned", "pending_confirmation", "blocked"] +StewardActionExecutionStatus = Literal["succeeded", "blocked", "needs_confirmation", "failed"] StewardTaskStatus = Literal[ "planned", "needs_confirmation", @@ -58,6 +74,17 @@ class StewardThinkingEvent(BaseModel): status: str = Field(default="completed", description="事件状态。") +class StewardActionStep(BaseModel): + step_id: str = Field(description="动作步骤 ID。") + action_type: StewardActionType = Field(description="白名单动作类型。") + label: str = Field(description="用户可见动作名称。") + target_task_id: str = Field(default="", description="关联的小财管家任务 ID。") + status: StewardActionStatus = Field(default="planned", description="动作规划状态。") + requires_confirmation: bool = Field(default=False, description="执行前是否需要用户确认。") + depends_on: list[str] = Field(default_factory=list, description="前置动作步骤 ID。") + payload: dict[str, Any] = Field(default_factory=dict, description="动作执行需要的确定性载荷。") + + class StewardTask(BaseModel): task_id: str = Field(description="小财管家任务 ID。") task_type: StewardTaskType = Field(description="任务类型。") @@ -66,9 +93,11 @@ class StewardTask(BaseModel): summary: str = Field(description="任务摘要。") status: StewardTaskStatus = Field(default="needs_confirmation", description="任务状态。") confidence: float = Field(default=0.0, ge=0.0, le=1.0, description="识别置信度。") + requested_action: StewardRequestedAction = Field(default="preview", description="用户希望执行的动作:预览、保存草稿或提交。") ontology_fields: dict[str, str] = Field(default_factory=dict, description="归一化后的业务本体字段。") missing_fields: list[str] = Field(default_factory=list, description="仍缺失的本体字段。") confirmation_required: bool = Field(default=True, description="执行前是否需要用户确认。") + action_steps: list[StewardActionStep] = Field(default_factory=list, description="当前任务的白名单动作步骤。") class StewardAttachmentGroup(BaseModel): @@ -120,6 +149,7 @@ class StewardPlanResponse(BaseModel): summary: str = Field(description="计划摘要。") thinking_events: list[StewardThinkingEvent] = Field(default_factory=list, description="过程摘要事件。") tasks: list[StewardTask] = Field(default_factory=list, description="拆解后的任务。") + action_steps: list[StewardActionStep] = Field(default_factory=list, description="本计划的全量白名单动作步骤。") attachment_groups: list[StewardAttachmentGroup] = Field(default_factory=list, description="附件归集建议。") confirmation_groups: list[StewardConfirmationAction] = Field(default_factory=list, description="等待用户确认的动作。") pending_flow_confirmation: StewardPendingFlowConfirmation = Field( @@ -134,6 +164,30 @@ class StewardPlanResponse(BaseModel): ) +class StewardActionExecuteRequest(BaseModel): + action_type: str = Field(description="待执行的白名单动作类型。未知动作会被 executor 阻断。") + message: str = Field(default="", description="触发该动作的用户原始话术。") + plan_id: str = Field(default="", description="关联的小财管家计划 ID。") + conversation_id: str | None = Field(default=None, description="关联会话 ID。") + task: StewardTask | None = Field(default=None, description="动作所属任务快照。") + action_step: StewardActionStep | None = Field(default=None, description="规划侧生成的动作步骤快照。") + confirmed: bool = Field(default=False, description="用户是否已确认执行该动作。") + context_json: dict[str, Any] = Field(default_factory=dict, description="前端或运行时补充上下文。") + client_trace_id: str = Field(default="", description="前端幂等或追踪 ID。") + + +class StewardActionExecuteResponse(BaseModel): + action_type: str = Field(description="实际处理的动作类型。") + status: StewardActionExecutionStatus = Field(description="动作执行状态。") + execution_source: str = Field(default="langgraph_action", description="执行来源或兜底来源。") + fallback_used: bool = Field(default=False, description="是否走了规则兜底执行。") + requires_confirmation: bool = Field(default=False, description="是否仍需用户确认。") + message: str = Field(description="面向用户展示的执行结果。") + blocked_reasons: list[str] = Field(default_factory=list, description="阻断原因。") + result_payload: dict[str, Any] = Field(default_factory=dict, description="业务服务返回的结构化结果。") + trace: list[dict[str, Any]] = Field(default_factory=list, description="动作执行轨迹。") + + class StewardSlotOption(BaseModel): label: str = Field(description="用户可见选项文案。") value: str = Field(description="写回本体字段的选项值。") diff --git a/server/src/app/services/application_fact_resolver.py b/server/src/app/services/application_fact_resolver.py index 0654c4b..eb4a2e8 100644 --- a/server/src/app/services/application_fact_resolver.py +++ b/server/src/app/services/application_fact_resolver.py @@ -3,7 +3,6 @@ from __future__ import annotations import re from datetime import date, timedelta - CITY_NAMES = ( "北京", "上海", @@ -34,6 +33,16 @@ MONTH_DAY_PATTERN = re.compile(r"(?P\d{1,2})\s*月\s*(?P\d{1,2})\s*( ISO_DATE_PATTERN = re.compile( r"(?P\d{4})[-/年](?P\d{1,2})[-/月](?P\d{1,2})(?:日)?" ) +ISO_DATE_RANGE_PATTERN = re.compile( + r"(?P\d{4})[-/年](?P\d{1,2})[-/月](?P\d{1,2})(?:日)?" + r"(?:至|到|~|~|—|--)" + r"(?P\d{4})[-/年](?P\d{1,2})[-/月](?P\d{1,2})(?:日)?" +) +MONTH_DAY_RANGE_PATTERN = re.compile( + r"(?P\d{1,2})\s*月\s*(?P\d{1,2})\s*(?:日|号)?" + r"(?:至|到|~|~|—|--|-)" + r"(?:(?P\d{1,2})\s*月\s*)?(?P\d{1,2})\s*(?:日|号)?" +) class ApplicationFactResolver: @@ -60,6 +69,38 @@ class ApplicationFactResolver: if "后天" in compact: return (base_date + timedelta(days=2)).isoformat() + iso_range = ISO_DATE_RANGE_PATTERN.search(compact) + if iso_range: + start_date = ApplicationFactResolver.safe_date( + int(iso_range.group("start_year")), + int(iso_range.group("start_month")), + int(iso_range.group("start_day")), + ) + end_date = ApplicationFactResolver.safe_date( + int(iso_range.group("end_year")), + int(iso_range.group("end_month")), + int(iso_range.group("end_day")), + ) + if start_date and end_date: + return f"{start_date} 至 {end_date}" + + month_day_range = MONTH_DAY_RANGE_PATTERN.search(compact) + if month_day_range: + start_month = int(month_day_range.group("start_month")) + end_month = int(month_day_range.group("end_month") or start_month) + start_date = ApplicationFactResolver.safe_date( + base_date.year, + start_month, + int(month_day_range.group("start_day")), + ) + end_date = ApplicationFactResolver.safe_date( + base_date.year, + end_month, + int(month_day_range.group("end_day")), + ) + if start_date and end_date: + return f"{start_date} 至 {end_date}" + iso_match = ISO_DATE_PATTERN.search(compact) if iso_match: return ApplicationFactResolver.safe_date( @@ -100,6 +141,9 @@ class ApplicationFactResolver: def extract_reason(segment: str, task_type: str) -> str: cleaned = re.sub(r"\s+", "", segment).strip(",,。;; ") if task_type == "expense_application": + normalized = strip_application_non_reason_tokens(cleaned) + if normalized: + return normalized match = re.search(r"(辅助|支持|协助|支撑|参加|拜访|调研|实施|部署|审核).+", cleaned) if match: return strip_trailing_connectors(match.group(0)) @@ -130,6 +174,47 @@ def strip_trailing_connectors(value: str) -> str: return re.sub(r"(?:并且|而且|同时|另外|还需要|需要)$", "", cleaned).strip(",,。;; ") +def strip_application_non_reason_tokens(value: str) -> str: + cleaned = str(value or "").strip(",,。;; ") + if not cleaned: + return "" + + preserved_transport_phrases: dict[str, str] = {} + + def preserve_transport_phrase(match: re.Match[str]) -> str: + placeholder = f"__TRANSPORT_REASON_{len(preserved_transport_phrases)}__" + preserved_transport_phrases[placeholder] = match.group(0) + return placeholder + + cleaned = re.sub( + r"(?:高铁|动车|火车|飞机|机票|航班|轮船|出租车|的士|网约车|打车|地铁|公交)往返", + preserve_transport_phrase, + cleaned, + ) + cleaned = ISO_DATE_RANGE_PATTERN.sub("", cleaned) + cleaned = MONTH_DAY_RANGE_PATTERN.sub("", cleaned) + cleaned = ISO_DATE_PATTERN.sub("", cleaned) + cleaned = MONTH_DAY_PATTERN.sub("", cleaned) + cleaned = re.sub(r"昨天|前天|明天|后天|今天|上周|上月|下周|下月|近期|月底", "", cleaned) + cleaned = re.sub(r"[0-9一二两三四五六七八九十]+天", "", cleaned) + cleaned = re.sub(fr"(?:去|到|赴|前往)(?:{'|'.join(CITY_NAMES)})", "", cleaned) + cleaned = re.sub(fr"(?:出差|差旅)(?:{'|'.join(CITY_NAMES)})", "", cleaned) + cleaned = re.sub(fr"(?:{'|'.join(CITY_NAMES)})(?=出差|差旅)", "", cleaned) + cleaned = re.sub(r"出差|差旅|费用申请|出差申请|差旅申请", "", cleaned) + cleaned = re.sub(r"交通方式?|出行方式?", "", cleaned) + cleaned = re.sub(r"高铁|动车|火车|飞机|机票|航班|轮船|出租车|的士|网约车|打车|地铁|公交", "", cleaned) + cleaned = re.sub( + r"保存草稿|存草稿|先保存|直接提交|提交申请|确认提交|提交审批|保存|提交|申请|发起|创建|我想要|我想|我要|请帮我|帮我", + "", + cleaned, + ) + cleaned = re.sub(r"(^|[,,。;;、])(?:交通|出行)(?=$|[,,。;;、])", r"\1", cleaned) + for placeholder, phrase in preserved_transport_phrases.items(): + cleaned = cleaned.replace(placeholder, phrase) + cleaned = re.sub(r"[,,。;;、]+", ",", cleaned).strip(",,。;;、的 ") + return strip_trailing_connectors(cleaned) + + def resolve_application_facts(segment: str, task_type: str, base_date: date) -> dict[str, str]: fields = { "expense_type": ApplicationFactResolver.infer_expense_type(segment, task_type), diff --git a/server/src/app/services/runtime_chat.py b/server/src/app/services/runtime_chat.py index c4a5cb2..dff23c8 100644 --- a/server/src/app/services/runtime_chat.py +++ b/server/src/app/services/runtime_chat.py @@ -244,6 +244,7 @@ class RuntimeChatService: timeout_seconds: int | None = None, slot_timeouts: dict[str, int] | None = None, max_attempts: int | None = None, + use_failure_cooldown: bool = True, ) -> RuntimeToolCallResult: configs: list[dict[str, str]] = [] calls: list[RuntimeChatCallTrace] = [] @@ -272,7 +273,7 @@ class RuntimeChatService: for attempt in range(1, resolved_max_attempts + 1): for config in configs: cache_key = self._build_slot_cache_key(config) - if _slot_failure_until.get(cache_key, 0.0) > monotonic(): + if use_failure_cooldown and _slot_failure_until.get(cache_key, 0.0) > monotonic(): logger.info( "Skip runtime chat tool slot=%s provider=%s because it is in cooldown", config["slot"], @@ -330,9 +331,10 @@ class RuntimeChatService: ) except Exception as exc: duration_ms = int((monotonic() - started) * 1000) - _slot_failure_until[cache_key] = ( - monotonic() + DEFAULT_RUNTIME_CHAT_FAILURE_COOLDOWN_SECONDS - ) + if use_failure_cooldown: + _slot_failure_until[cache_key] = ( + monotonic() + DEFAULT_RUNTIME_CHAT_FAILURE_COOLDOWN_SECONDS + ) calls.append( RuntimeChatCallTrace( slot=config["slot"], diff --git a/server/src/app/services/steward_action_contracts.py b/server/src/app/services/steward_action_contracts.py new file mode 100644 index 0000000..31a66ae --- /dev/null +++ b/server/src/app/services/steward_action_contracts.py @@ -0,0 +1,224 @@ +from __future__ import annotations + +from typing import Any + +from app.schemas.steward import ( + StewardActionStatus, + StewardActionStep, + StewardPlanResponse, + StewardTask, +) + + +class StewardActionPlanBuilder: + """把小财管家任务转换为确定性的白名单动作步骤。""" + + def attach_action_steps(self, plan: StewardPlanResponse) -> StewardPlanResponse: + if not plan.tasks: + return plan.model_copy(update={"action_steps": []}) + + tasks: list[StewardTask] = [] + plan_steps = [self._build_detect_intent_step(plan)] + for task in plan.tasks: + task_steps = self.build_task_action_steps(task) + tasks.append(task.model_copy(update={"action_steps": task_steps})) + plan_steps.extend(task_steps) + return plan.model_copy(update={"tasks": tasks, "action_steps": plan_steps}) + + def build_task_action_steps(self, task: StewardTask) -> list[StewardActionStep]: + if task.task_type == "expense_application": + return self._build_application_steps(task) + return self._build_reimbursement_steps(task) + + def _build_application_steps(self, task: StewardTask) -> list[StewardActionStep]: + steps = [ + self._build_task_step( + task, + 1, + "fill_application_fields", + "填充申请字段", + payload=self._field_payload(task), + ), + self._build_task_step( + task, + 2, + "build_application_preview", + "展示申请核对表", + depends_on_index=1, + payload={"task_id": task.task_id, "ontology_fields": task.ontology_fields}, + ), + self._build_task_step( + task, + 3, + "validate_required_fields", + "校验申请必填字段", + status=self._validation_status(task), + depends_on_index=2, + payload={"missing_fields": task.missing_fields}, + ), + ] + if task.requested_action == "save_draft": + steps.append( + self._build_task_step( + task, + 4, + "save_application_draft", + "保存申请草稿", + status=self._side_effect_status(task, requires_confirmation=False), + depends_on_index=3, + payload={ + "task_id": task.task_id, + "requested_action": task.requested_action, + "ontology_fields": task.ontology_fields, + }, + ) + ) + elif task.requested_action == "submit": + steps.append( + self._build_task_step( + task, + 4, + "run_duplicate_precheck", + "检查重复或冲突申请", + status=self._side_effect_status(task, requires_confirmation=False), + depends_on_index=3, + payload={"task_id": task.task_id, "precheck_type": "travel_overlap"}, + ) + ) + steps.append( + self._build_task_step( + task, + 5, + "submit_application", + "提交申请审批", + status=self._side_effect_status(task, requires_confirmation=True), + requires_confirmation=not bool(task.missing_fields), + depends_on_index=4, + payload={ + "task_id": task.task_id, + "requested_action": task.requested_action, + "confirmation_required": True, + }, + ) + ) + return steps + + def _build_reimbursement_steps(self, task: StewardTask) -> list[StewardActionStep]: + steps = [ + self._build_task_step( + task, + 1, + "fill_reimbursement_fields", + "填充报销字段", + payload=self._field_payload(task), + ), + self._build_task_step( + task, + 2, + "build_reimbursement_preview", + "展示报销核对表", + depends_on_index=1, + payload={"task_id": task.task_id, "ontology_fields": task.ontology_fields}, + ), + self._build_task_step( + task, + 3, + "validate_required_fields", + "校验报销必填字段", + status=self._validation_status(task), + depends_on_index=2, + payload={"missing_fields": task.missing_fields}, + ), + self._build_task_step( + task, + 4, + "create_reimbursement_draft", + "创建报销草稿", + status=self._side_effect_status(task, requires_confirmation=False), + depends_on_index=3, + payload={ + "task_id": task.task_id, + "requested_action": task.requested_action, + "ontology_fields": task.ontology_fields, + }, + ), + ] + if task.ontology_fields.get("attachments"): + steps.append( + self._build_task_step( + task, + 5, + "associate_attachments", + "关联报销附件", + status=self._side_effect_status(task, requires_confirmation=False), + depends_on_index=4, + payload={"task_id": task.task_id, "attachments": task.ontology_fields["attachments"]}, + ) + ) + return steps + + @staticmethod + def _build_detect_intent_step(plan: StewardPlanResponse) -> StewardActionStep: + return StewardActionStep( + step_id="plan:00:detect_intent", + action_type="detect_intent", + label="识别业务意图", + status="completed", + payload={ + "planning_source": plan.planning_source, + "plan_status": plan.plan_status, + }, + ) + + def _build_task_step( + self, + task: StewardTask, + index: int, + action_type: str, + label: str, + *, + status: StewardActionStatus = "planned", + requires_confirmation: bool = False, + depends_on_index: int | None = None, + payload: dict[str, Any] | None = None, + ) -> StewardActionStep: + step_id = self._step_id(task, index) + depends_on = [self._step_id(task, depends_on_index)] if depends_on_index is not None else [] + return StewardActionStep( + step_id=step_id, + action_type=action_type, # type: ignore[arg-type] + label=label, + target_task_id=task.task_id, + status=status, + requires_confirmation=requires_confirmation, + depends_on=depends_on, + payload=payload or {}, + ) + + @staticmethod + def _field_payload(task: StewardTask) -> dict[str, Any]: + return { + "task_id": task.task_id, + "task_type": task.task_type, + "requested_action": task.requested_action, + "ontology_fields": task.ontology_fields, + "missing_fields": task.missing_fields, + } + + @staticmethod + def _validation_status(task: StewardTask) -> StewardActionStatus: + return "blocked" if task.missing_fields else "planned" + + @staticmethod + def _side_effect_status( + task: StewardTask, + *, + requires_confirmation: bool, + ) -> StewardActionStatus: + if task.missing_fields: + return "blocked" + return "pending_confirmation" if requires_confirmation else "planned" + + @staticmethod + def _step_id(task: StewardTask, index: int) -> str: + return f"{task.task_id}:{index:02d}" diff --git a/server/src/app/services/steward_action_executor.py b/server/src/app/services/steward_action_executor.py new file mode 100644 index 0000000..3b22b79 --- /dev/null +++ b/server/src/app/services/steward_action_executor.py @@ -0,0 +1,570 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any + +from sqlalchemy.orm import Session + +from app.api.deps import CurrentUserContext +from app.schemas.ontology import OntologyParseResult, OntologyPermission +from app.schemas.steward import ( + StewardActionExecuteRequest, + StewardActionExecuteResponse, + StewardTask, +) +from app.schemas.user_agent import UserAgentRequest +from app.services.attachment_association_jobs import AttachmentAssociationJobRunner +from app.services.expense_claims import ExpenseClaimService +from app.services.user_agent import UserAgentService +from app.services.user_agent_application_dates import resolve_application_days_from_time_range + +SUPPORTED_ACTIONS = { + "fill_application_fields", + "build_application_preview", + "fill_reimbursement_fields", + "build_reimbursement_preview", + "validate_required_fields", + "run_duplicate_precheck", + "save_application_draft", + "submit_application", + "create_reimbursement_draft", + "link_existing_application", + "associate_attachments", +} +APPLICATION_SIDE_EFFECT_ACTIONS = {"save_application_draft", "submit_application"} +REIMBURSEMENT_SIDE_EFFECT_ACTIONS = {"create_reimbursement_draft", "link_existing_application"} +NOOP_ACTIONS = { + "fill_application_fields", + "build_application_preview", + "fill_reimbursement_fields", + "build_reimbursement_preview", + "validate_required_fields", +} +TRANSPORT_MODE_LABELS = { + "train": "火车", + "rail": "火车", + "high_speed_rail": "火车", + "flight": "飞机", + "airplane": "飞机", + "plane": "飞机", + "ship": "轮船", + "boat": "轮船", + "taxi": "出租车", +} +EXPENSE_TYPE_LABELS = { + "travel": "差旅费", + "transport": "交通费", + "hotel": "住宿费", + "meal": "业务招待费", + "meeting": "会务费", + "office": "办公用品费", + "other": "其他费用", +} +APPLICATION_TYPE_LABELS = { + "travel": "差旅费用申请", + "travel_application": "差旅费用申请", + "transport": "交通费用申请", + "hotel": "住宿费用申请", + "meeting": "会务费用申请", + "office": "办公费用申请", +} + + +class StewardActionExecutor: + """执行 LangGraph 规划出的确定性白名单动作。""" + + def __init__(self, db: Session) -> None: + self.db = db + + def execute( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + ) -> StewardActionExecuteResponse: + action_type = self._normalize_action_type(request.action_type) + trace = [self._trace("received", action_type=action_type, plan_id=request.plan_id)] + if action_type not in SUPPORTED_ACTIONS: + return self._blocked( + action_type, + f"不支持的小财管家动作:{action_type or '空动作'}。", + trace=[*trace, self._trace("blocked", reason="unsupported_action")], + ) + + task = request.task + if task is None and action_type not in NOOP_ACTIONS: + return self._blocked( + action_type, + "动作缺少任务快照,无法安全执行。", + trace=[*trace, self._trace("blocked", reason="missing_task")], + ) + + blocked_reasons = self._resolve_task_blockers(task) + if blocked_reasons: + return self._blocked( + action_type, + "当前任务仍有必填信息缺失,已停止执行动作。", + blocked_reasons=blocked_reasons, + trace=[*trace, self._trace("blocked", reason="missing_fields")], + ) + + if action_type in NOOP_ACTIONS: + return StewardActionExecuteResponse( + action_type=action_type, + status="succeeded", + message="动作已确认,无需调用外部业务服务。", + result_payload={ + "task_id": task.task_id if task is not None else "", + "action_type": action_type, + }, + trace=[*trace, self._trace("completed", mode="noop")], + ) + if action_type == "run_duplicate_precheck": + return self._run_duplicate_precheck(request, current_user, trace) + if action_type in APPLICATION_SIDE_EFFECT_ACTIONS: + return self._execute_application_action(request, current_user, action_type, trace) + if action_type in REIMBURSEMENT_SIDE_EFFECT_ACTIONS: + return self._execute_reimbursement_action(request, current_user, action_type, trace) + if action_type == "associate_attachments": + return self._execute_associate_attachments_action(request, current_user, trace) + + return self._blocked( + action_type, + f"动作 {action_type} 暂未接入执行器。", + trace=[*trace, self._trace("blocked", reason="unwired_action")], + ) + + def _run_duplicate_precheck( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + trace: list[dict[str, Any]], + ) -> StewardActionExecuteResponse: + payload = self._build_application_user_agent_request( + request, + current_user, + action_type="submit_application", + force_submit_message=False, + ) + service = UserAgentService(self.db) + facts = service._resolve_expense_application_facts(payload) + duplicate = service._find_duplicate_expense_application_record(payload, facts) + if duplicate is not None: + result_payload = { + "status": "blocked", + "blocking": True, + "duplicate_claim_id": str(duplicate.id or ""), + "duplicate_claim_no": str(duplicate.claim_no or ""), + "duplicate_stage": str(duplicate.approval_stage or ""), + } + return StewardActionExecuteResponse( + action_type="run_duplicate_precheck", + status="blocked", + message="检测到同一申请人、同一申请类型、同一时间段已有申请单,已停止直接提交。", + blocked_reasons=["duplicate_application"], + result_payload=result_payload, + trace=[*trace, self._trace("blocked", reason="duplicate_application")], + ) + result_payload = {"status": "ok", "blocking": False} + return StewardActionExecuteResponse( + action_type="run_duplicate_precheck", + status="succeeded", + message="未发现重复或冲突申请,可以继续提交。", + result_payload=result_payload, + trace=[*trace, self._trace("completed", mode="duplicate_precheck")], + ) + + def _execute_application_action( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + action_type: str, + trace: list[dict[str, Any]], + ) -> StewardActionExecuteResponse: + if action_type == "submit_application" and not request.confirmed: + return StewardActionExecuteResponse( + action_type=action_type, + status="needs_confirmation", + requires_confirmation=True, + message="提交申请前需要用户确认。请确认申请核对表无误后再提交。", + trace=[*trace, self._trace("needs_confirmation")], + ) + if action_type == "submit_application": + precheck_blocker = self._resolve_submit_precheck_blocker(request.context_json) + if precheck_blocker: + return self._blocked( + action_type, + precheck_blocker, + blocked_reasons=["precheck_not_passed"], + trace=[*trace, self._trace("blocked", reason="precheck_not_passed")], + ) + + payload = self._build_application_user_agent_request( + request, + current_user, + action_type=action_type, + force_submit_message=action_type == "submit_application", + ) + try: + user_agent_response = UserAgentService(self.db)._build_expense_application_response( + payload, + risk_flags=[], + ) + except ValueError as exc: + return self._failed(action_type, str(exc), trace) + + draft_payload = ( + user_agent_response.draft_payload.model_dump(mode="json") + if user_agent_response.draft_payload is not None + else None + ) + result_payload = { + "answer": user_agent_response.answer, + "suggested_actions": [ + action.model_dump(mode="json") + for action in user_agent_response.suggested_actions + ], + "requires_confirmation": user_agent_response.requires_confirmation, + "draft_payload": draft_payload, + } + status = "succeeded" if draft_payload is not None else "blocked" + blocked_reasons = [] if draft_payload is not None else ["application_not_persisted"] + return StewardActionExecuteResponse( + action_type=action_type, + status=status, + message=user_agent_response.answer, + blocked_reasons=blocked_reasons, + result_payload=result_payload, + trace=[*trace, self._trace("completed", service="UserAgentService")], + ) + + def _execute_reimbursement_action( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + action_type: str, + trace: list[dict[str, Any]], + ) -> StewardActionExecuteResponse: + context_json = self._build_reimbursement_context_json(request, current_user) + run_id = self._build_run_id(request, action_type) + ontology = OntologyParseResult( + scenario="expense", + intent="draft", + permission=OntologyPermission( + level="draft_write", + allowed=True, + reason="小财管家白名单动作创建报销草稿。", + ), + confidence=1.0, + run_id=run_id, + ) + try: + result = ExpenseClaimService(self.db).save_or_submit_from_ontology( + run_id=run_id, + user_id=current_user.username, + message=self._resolve_message(request), + ontology=ontology, + context_json=context_json, + ) + except ValueError as exc: + return self._failed(action_type, str(exc), trace) + + persisted = str(result.get("claim_id") or "").strip() and str(result.get("status") or "").strip() == "draft" + return StewardActionExecuteResponse( + action_type=action_type, + status="succeeded" if persisted else "blocked", + message=str(result.get("message") or "报销草稿已生成。").strip(), + blocked_reasons=[] if persisted else ["reimbursement_not_persisted"], + result_payload=result, + trace=[*trace, self._trace("completed", service="ExpenseClaimService")], + ) + + def _execute_associate_attachments_action( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + trace: list[dict[str, Any]], + ) -> StewardActionExecuteResponse: + receipt_ids = self._resolve_receipt_ids(request) + if not receipt_ids: + return self._blocked( + "associate_attachments", + "关联附件前需要先提供票据夹 receipt_ids。", + blocked_reasons=["missing_receipt_ids"], + trace=[*trace, self._trace("blocked", reason="missing_receipt_ids")], + ) + try: + result = AttachmentAssociationJobRunner(self.db).run( + receipt_ids=receipt_ids, + current_user=current_user, + ) + except ValueError as exc: + return self._blocked( + "associate_attachments", + str(exc), + blocked_reasons=["attachment_association_blocked"], + trace=[*trace, self._trace("blocked", reason="attachment_association_blocked")], + ) + except Exception as exc: + return self._failed("associate_attachments", str(exc), trace) + + return StewardActionExecuteResponse( + action_type="associate_attachments", + status="succeeded", + message=str( + result.get("message") + or f"已自动关联到 {result.get('claim_no') or '报销草稿'}。" + ).strip(), + result_payload={ + **dict(result or {}), + "receipt_ids": receipt_ids, + }, + trace=[*trace, self._trace("completed", service="AttachmentAssociationJobRunner")], + ) + + def _build_application_user_agent_request( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + *, + action_type: str, + force_submit_message: bool, + ) -> UserAgentRequest: + run_id = self._build_run_id(request, action_type) + context_json = self._build_application_context_json(request, current_user, action_type) + message = self._resolve_message(request) + if force_submit_message and "确认提交" not in message and "直接提交" not in message: + message = "\n".join([message, "确认提交"]).strip() + ontology = OntologyParseResult( + scenario="expense", + intent="operate", + permission=OntologyPermission( + level="approval_required", + allowed=True, + reason="小财管家白名单动作执行申请操作。", + ), + confidence=1.0, + run_id=run_id, + ) + return UserAgentRequest( + run_id=run_id, + user_id=current_user.username, + message=message, + ontology=ontology, + context_json=context_json, + tool_payload={}, + selected_capability_codes=[], + degraded=False, + requires_confirmation=False, + ) + + def _build_application_context_json( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + action_type: str, + ) -> dict[str, Any]: + fields = self._resolve_ontology_fields(request.task) + preview_fields = { + "applicationType": self._resolve_application_type_label(fields.get("expense_type")), + "time": str(fields.get("time_range") or ""), + "location": str(fields.get("location") or ""), + "reason": str(fields.get("reason") or ""), + "days": self._resolve_days_text(fields.get("time_range")), + "transportMode": self._resolve_transport_label(fields.get("transport_mode")), + "amount": str(fields.get("amount") or ""), + "applicant": current_user.name, + "department": current_user.department_name, + "position": current_user.position, + "grade": current_user.grade, + "managerName": current_user.manager_name, + } + context_json = { + **dict(request.context_json or {}), + "session_type": "application", + "entry_source": "steward_action_executor", + "document_type": "expense_application", + "application_stage": "expense_application", + "role_codes": current_user.role_codes, + "is_admin": current_user.is_admin, + "username": current_user.username, + "name": current_user.name, + "department_name": current_user.department_name, + "position": current_user.position, + "grade": current_user.grade, + "employee_no": current_user.employee_no, + "manager_name": current_user.manager_name, + "application_preview": {"fields": preview_fields}, + } + if action_type == "save_application_draft": + context_json["application_action"] = "save_draft" + context_json["application_save_mode"] = True + return context_json + + def _build_reimbursement_context_json( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + ) -> dict[str, Any]: + fields = self._resolve_ontology_fields(request.task) + review_form_values = { + "expense_type": self._resolve_expense_type_label(fields.get("expense_type")), + "time_range": str(fields.get("time_range") or ""), + "occurred_date": str(fields.get("time_range") or ""), + "location": str(fields.get("location") or ""), + "reason": str(fields.get("reason") or ""), + "amount": str(fields.get("amount") or ""), + "transport_mode": self._resolve_transport_label(fields.get("transport_mode")), + "attachments": str(fields.get("attachments") or ""), + } + return { + **dict(request.context_json or {}), + "session_type": "expense", + "entry_source": "steward_action_executor", + "review_action": "save_draft", + "review_form_values": review_form_values, + "user_input_text": self._resolve_message(request), + "role_codes": current_user.role_codes, + "is_admin": current_user.is_admin, + "username": current_user.username, + "name": current_user.name, + "department_name": current_user.department_name, + "position": current_user.position, + "grade": current_user.grade, + "employee_no": current_user.employee_no, + "manager_name": current_user.manager_name, + } + + @staticmethod + def _resolve_receipt_ids(request: StewardActionExecuteRequest) -> list[str]: + context_json = dict(request.context_json or {}) + raw_values = context_json.get("receipt_ids") or context_json.get("receiptIds") or [] + if isinstance(raw_values, str): + raw_values = [item.strip() for item in raw_values.split(",")] + if not isinstance(raw_values, list): + raw_values = [] + receipt_ids = [ + str(item or "").strip() + for item in raw_values + if str(item or "").strip() + ] + if receipt_ids: + return list(dict.fromkeys(receipt_ids)) + fields = StewardActionExecutor._resolve_ontology_fields(request.task) + raw_receipt = str(fields.get("receipt_ids") or fields.get("receiptIds") or "").strip() + if not raw_receipt: + return [] + return list(dict.fromkeys(item.strip() for item in raw_receipt.split(",") if item.strip())) + + @staticmethod + def _resolve_submit_precheck_blocker(context_json: dict[str, Any]) -> str: + precheck = context_json.get("precheck_result") or context_json.get("precheckResult") + if not isinstance(precheck, dict): + return "提交申请前需要先完成重复/冲突预检查。" + if bool(precheck.get("blocking")): + return str(precheck.get("summary") or "重复/冲突预检查未通过,已停止提交。").strip() + status = str(precheck.get("status") or "").strip().lower() + if status not in {"ok", "passed", "succeeded"}: + return "重复/冲突预检查未通过,已停止提交。" + return "" + + @staticmethod + def _resolve_task_blockers(task: StewardTask | None) -> list[str]: + if task is None: + return [] + return [ + str(field or "").strip() + for field in list(task.missing_fields or []) + if str(field or "").strip() + ] + + @staticmethod + def _resolve_ontology_fields(task: StewardTask | None) -> dict[str, str]: + if task is None or not isinstance(task.ontology_fields, dict): + return {} + return { + str(key or "").strip(): str(value or "").strip() + for key, value in task.ontology_fields.items() + if str(key or "").strip() and str(value or "").strip() + } + + @staticmethod + def _resolve_message(request: StewardActionExecuteRequest) -> str: + message = str(request.message or "").strip() + if message: + return message + if request.task is not None: + return str(request.task.summary or request.task.title or "").strip() + return "小财管家动作执行" + + @staticmethod + def _resolve_transport_label(value: Any) -> str: + text = str(value or "").strip() + return TRANSPORT_MODE_LABELS.get(text.lower(), text) + + @staticmethod + def _resolve_application_type_label(value: Any) -> str: + text = str(value or "").strip() + return APPLICATION_TYPE_LABELS.get(text.lower(), text or "费用申请") + + @staticmethod + def _resolve_expense_type_label(value: Any) -> str: + text = str(value or "").strip() + return EXPENSE_TYPE_LABELS.get(text.lower(), text or "其他费用") + + @staticmethod + def _resolve_days_text(value: Any) -> str: + days = resolve_application_days_from_time_range(str(value or "")) + return f"{days}天" if days else "" + + @staticmethod + def _normalize_action_type(value: str) -> str: + return str(value or "").strip() + + @staticmethod + def _build_run_id(request: StewardActionExecuteRequest, action_type: str) -> str: + trace_id = str(request.client_trace_id or "").strip() + if trace_id: + return f"steward-action:{trace_id}" + task_id = str(request.task.task_id if request.task is not None else "").strip() + suffix = task_id or datetime.now(UTC).strftime("%Y%m%d%H%M%S%f") + return f"steward-action:{action_type}:{suffix}" + + @staticmethod + def _trace(stage: str, **extra: Any) -> dict[str, Any]: + return { + "stage": stage, + "at": datetime.now(UTC).isoformat(), + **extra, + } + + @staticmethod + def _blocked( + action_type: str, + message: str, + *, + blocked_reasons: list[str] | None = None, + trace: list[dict[str, Any]] | None = None, + ) -> StewardActionExecuteResponse: + return StewardActionExecuteResponse( + action_type=action_type, + status="blocked", + execution_source="rule_fallback", + fallback_used=True, + message=message, + blocked_reasons=blocked_reasons or [message], + trace=trace or [], + ) + + def _failed( + self, + action_type: str, + message: str, + trace: list[dict[str, Any]], + ) -> StewardActionExecuteResponse: + return StewardActionExecuteResponse( + action_type=action_type, + status="failed", + message=message or "动作执行失败。", + blocked_reasons=[message] if message else [], + trace=[*trace, self._trace("failed", reason=message)], + ) diff --git a/server/src/app/services/steward_graph_action_runtime.py b/server/src/app/services/steward_graph_action_runtime.py new file mode 100644 index 0000000..e9d9979 --- /dev/null +++ b/server/src/app/services/steward_graph_action_runtime.py @@ -0,0 +1,204 @@ +from __future__ import annotations + +from datetime import UTC, datetime +from typing import Any, TypedDict + +from langgraph.graph import END, START, StateGraph +from sqlalchemy.orm import Session + +from app.api.deps import CurrentUserContext +from app.models.agent_conversation import AgentConversation +from app.schemas.steward import StewardActionExecuteRequest, StewardActionExecuteResponse +from app.services.agent_conversations import AgentConversationService +from app.services.steward_action_executor import StewardActionExecutor + +ACTION_CHECKPOINT_KEY = "steward_action_checkpoint" +TERMINAL_ACTION_STATUSES = {"succeeded", "blocked", "failed"} + + +class StewardGraphActionState(TypedDict, total=False): + request: StewardActionExecuteRequest + current_user: CurrentUserContext + conversation: AgentConversation | None + trace_id: str + existing_result: dict[str, Any] + response: StewardActionExecuteResponse + + +class StewardGraphActionRuntime: + """用 LangGraph 包装小财管家白名单动作执行、checkpoint 和幂等重放。""" + + def __init__(self, db: Session) -> None: + self.db = db + self.executor = StewardActionExecutor(db) + self._graph = self._build_graph() + + def execute( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + ) -> StewardActionExecuteResponse: + final_state = self._graph.invoke( + { + "request": request, + "current_user": current_user, + } + ) + response = final_state.get("response") + if not isinstance(response, StewardActionExecuteResponse): + raise RuntimeError("LangGraph action runtime 未生成有效执行结果。") + return response + + def _build_graph(self): + graph = StateGraph(StewardGraphActionState) + graph.add_node("action_checkpoint_load", self._load_checkpoint) + graph.add_node("action_execute_node", self._execute_action) + graph.add_node("action_checkpoint_persist", self._persist_checkpoint) + + graph.add_edge(START, "action_checkpoint_load") + graph.add_conditional_edges( + "action_checkpoint_load", + self._route_after_checkpoint_load, + { + "replay": "action_checkpoint_persist", + "execute": "action_execute_node", + }, + ) + graph.add_edge("action_execute_node", "action_checkpoint_persist") + graph.add_edge("action_checkpoint_persist", END) + return graph.compile() + + def _load_checkpoint(self, state: StewardGraphActionState) -> dict[str, Any]: + request = state["request"] + conversation = self._get_or_create_conversation(request, state["current_user"]) + trace_id = self._resolve_trace_id(request) + if conversation is None or not trace_id: + return { + "conversation": conversation, + "trace_id": trace_id, + } + + checkpoint = self._resolve_checkpoint(conversation) + existing = dict(checkpoint.get("actions", {}).get(trace_id) or {}) + existing_response = existing.get("response") + status = str(existing.get("status") or "").strip() + if isinstance(existing_response, dict) and ( + status in TERMINAL_ACTION_STATUSES + or (status == "needs_confirmation" and not request.confirmed) + ): + response = StewardActionExecuteResponse.model_validate(existing_response) + response.result_payload = { + **dict(response.result_payload or {}), + "idempotent_replay": True, + } + response.trace = [ + *list(response.trace or []), + self._trace("checkpoint_replay", client_trace_id=trace_id), + ] + return { + "conversation": conversation, + "trace_id": trace_id, + "existing_result": existing, + "response": response, + } + + return { + "conversation": conversation, + "trace_id": trace_id, + "existing_result": existing, + } + + @staticmethod + def _route_after_checkpoint_load(state: StewardGraphActionState) -> str: + if isinstance(state.get("response"), StewardActionExecuteResponse): + return "replay" + return "execute" + + def _execute_action(self, state: StewardGraphActionState) -> dict[str, StewardActionExecuteResponse]: + response = self.executor.execute(state["request"], state["current_user"]) + return {"response": response} + + def _persist_checkpoint(self, state: StewardGraphActionState) -> dict[str, StewardActionExecuteResponse]: + response = state["response"] + conversation = state.get("conversation") + trace_id = str(state.get("trace_id") or "").strip() + if conversation is None or not trace_id: + return {"response": response} + + checkpoint = self._resolve_checkpoint(conversation) + actions = dict(checkpoint.get("actions") or {}) + request = state["request"] + response_payload = response.model_dump(mode="json") + actions[trace_id] = { + "client_trace_id": trace_id, + "action_type": response.action_type, + "status": response.status, + "request": request.model_dump(mode="json"), + "response": response_payload, + "updated_at": datetime.now(UTC).isoformat(), + } + checkpoint["actions"] = actions + if response.status == "needs_confirmation": + checkpoint["pending_interrupt"] = { + "client_trace_id": trace_id, + "action_type": response.action_type, + "message": response.message, + "requires_confirmation": True, + "updated_at": datetime.now(UTC).isoformat(), + } + elif str(checkpoint.get("pending_interrupt", {}).get("client_trace_id") or "") == trace_id: + checkpoint["pending_interrupt"] = {} + + conversation.state_json = { + **dict(conversation.state_json or {}), + ACTION_CHECKPOINT_KEY: checkpoint, + } + conversation.updated_at = datetime.now(UTC) + self.db.add(conversation) + self.db.commit() + return {"response": response} + + def _get_or_create_conversation( + self, + request: StewardActionExecuteRequest, + current_user: CurrentUserContext, + ) -> AgentConversation | None: + conversation_id = str(request.conversation_id or "").strip() + if not conversation_id: + return None + return AgentConversationService(self.db).get_or_create_conversation( + conversation_id=conversation_id, + user_id=current_user.username, + source="user_message", + context_json={ + "session_type": "steward", + "entry_source": "steward_action_executor", + "steward_state": dict((request.context_json or {}).get("steward_state") or {}), + }, + ) + + @staticmethod + def _resolve_checkpoint(conversation: AgentConversation) -> dict[str, Any]: + checkpoint = dict((conversation.state_json or {}).get(ACTION_CHECKPOINT_KEY) or {}) + checkpoint.setdefault("actions", {}) + checkpoint.setdefault("pending_interrupt", {}) + return checkpoint + + @staticmethod + def _resolve_trace_id(request: StewardActionExecuteRequest) -> str: + trace_id = str(request.client_trace_id or "").strip() + if trace_id: + return trace_id + action_type = str(request.action_type or "").strip() + task_id = str(request.task.task_id if request.task is not None else "").strip() + if action_type and task_id: + return f"{action_type}:{task_id}" + return "" + + @staticmethod + def _trace(stage: str, **extra: Any) -> dict[str, Any]: + return { + "stage": stage, + "at": datetime.now(UTC).isoformat(), + **extra, + } diff --git a/server/src/app/services/steward_graph_planner.py b/server/src/app/services/steward_graph_planner.py new file mode 100644 index 0000000..4b32193 --- /dev/null +++ b/server/src/app/services/steward_graph_planner.py @@ -0,0 +1,220 @@ +from __future__ import annotations + +from datetime import date +from typing import Any, TypedDict + +from langgraph.graph import END, START, StateGraph + +from app.schemas.steward import StewardPlanRequest, StewardPlanResponse +from app.services.steward_action_contracts import StewardActionPlanBuilder +from app.services.steward_constants import BUSINESS_CANONICAL_FIELD_ORDER +from app.services.steward_intent_agent import StewardIntentAgent, StewardIntentAgentResult +from app.services.steward_model_plan_builder import StewardModelPlanBuilder +from app.services.steward_off_topic_agent import StewardOffTopicAgent +from app.services.steward_planner_extraction import StewardPlannerExtractionMixin +from app.services.steward_planner_fallback import StewardPlannerFallbackMixin + + +class StewardGraphState(TypedDict, total=False): + request: StewardPlanRequest + message: str + base_date: date + scenario: str | None + should_use_model: bool + intent_result: StewardIntentAgentResult | None + plan: StewardPlanResponse + model_call_traces: list[dict[str, Any]] + fallback_reason: str + + +class StewardGraphPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtractionMixin): + """用 LangGraph 编排小财管家的意图识别、流程判断和兜底路径。""" + + def __init__( + self, + intent_agent: StewardIntentAgent | None = None, + off_topic_agent: StewardOffTopicAgent | None = None, + ) -> None: + self.intent_agent = intent_agent + self.off_topic_agent = off_topic_agent + self._graph = self._build_graph() + + def build_plan(self, request: StewardPlanRequest) -> StewardPlanResponse: + final_state = self._graph.invoke( + { + "request": request, + "model_call_traces": [], + "fallback_reason": "", + } + ) + plan = final_state.get("plan") + if not isinstance(plan, StewardPlanResponse): + raise RuntimeError("LangGraph 小财管家规划未生成有效计划。") + return plan + + def _build_graph(self): + graph = StateGraph(StewardGraphState) + graph.add_node("prepare_context", self._prepare_context) + graph.add_node("detect_model_intent", self._detect_model_intent) + graph.add_node("build_off_topic_plan", self._build_off_topic_graph_plan) + graph.add_node("build_rule_fallback_plan", self._build_rule_fallback_graph_plan) + graph.add_node("attach_action_steps", self._attach_action_steps) + + graph.add_edge(START, "prepare_context") + graph.add_conditional_edges( + "prepare_context", + self._route_after_prepare_context, + { + "model": "detect_model_intent", + "off_topic": "build_off_topic_plan", + "fallback": "build_rule_fallback_plan", + }, + ) + graph.add_conditional_edges( + "detect_model_intent", + self._route_after_model_intent, + { + "done": "attach_action_steps", + "off_topic": "build_off_topic_plan", + "fallback": "build_rule_fallback_plan", + }, + ) + graph.add_edge("build_off_topic_plan", "attach_action_steps") + graph.add_edge("build_rule_fallback_plan", "attach_action_steps") + graph.add_edge("attach_action_steps", END) + return graph.compile() + + def _prepare_context(self, state: StewardGraphState) -> dict[str, Any]: + request = state["request"] + message = self._clean_text(request.message) + if not message: + raise ValueError("小财管家需要一段任务描述。") + + base_date = self._resolve_base_date(request.client_now_iso, request.context_json) + return { + "message": message, + "base_date": base_date, + "scenario": self._classify_irrelevant_input(message, request), + "should_use_model": bool( + self.intent_agent is not None + and self._should_use_model_intent_recognition(message, base_date, request) + ), + } + + @staticmethod + def _route_after_prepare_context(state: StewardGraphState) -> str: + if state.get("should_use_model"): + return "model" + if state.get("scenario") is not None: + return "off_topic" + return "fallback" + + def _detect_model_intent(self, state: StewardGraphState) -> dict[str, Any]: + request = state["request"] + message = state["message"] + base_date = state["base_date"] + model_call_traces: list[dict[str, Any]] = [] + + if self.intent_agent is None: + return {} + + try: + intent_result = self.intent_agent.detect( + request, + base_date=base_date, + canonical_fields=list(BUSINESS_CANONICAL_FIELD_ORDER), + ) + if intent_result is None: + return { + "model_call_traces": self._last_intent_call_traces(model_call_traces), + "fallback_reason": ( + "主模型未返回可用的 function calling 计划,已切换到规则兜底。" + ), + } + + model_call_traces = intent_result.model_call_traces + llm_plan = StewardModelPlanBuilder(self).build( + intent_result, + request=request, + base_date=base_date, + ) + if llm_plan is None: + return { + "model_call_traces": self._last_intent_call_traces(model_call_traces), + "fallback_reason": ( + "主模型未返回可用的 function calling 计划,已切换到规则兜底。" + ), + } + + if self._looks_like_ambiguous_travel_flow(message, base_date, request): + return { + "plan": self._build_pending_flow_fallback_plan( + request, + base_date=base_date, + model_call_traces=model_call_traces, + fallback_reason=( + "主模型返回了直接任务,但当前话术没有明确申请或报销动作;" + "服务端已改为候选流程确认,避免误入申请流程。" + ), + planning_source="llm_function_call", + ), + "model_call_traces": model_call_traces, + } + + return { + "intent_result": intent_result, + "plan": llm_plan, + "model_call_traces": model_call_traces, + } + except Exception as exc: + return { + "model_call_traces": self._last_intent_call_traces(model_call_traces), + "fallback_reason": f"主模型 function calling 调用失败,已切换到规则兜底:{exc}", + } + + @staticmethod + def _route_after_model_intent(state: StewardGraphState) -> str: + if isinstance(state.get("plan"), StewardPlanResponse): + return "done" + if state.get("scenario") is not None: + return "off_topic" + return "fallback" + + def _build_off_topic_graph_plan( + self, + state: StewardGraphState, + ) -> dict[str, StewardPlanResponse]: + return { + "plan": self._build_off_topic_plan( + state["request"], + scenario=str(state["scenario"] or ""), + model_call_traces=state.get("model_call_traces"), + fallback_reason=str(state.get("fallback_reason") or ""), + ) + } + + def _build_rule_fallback_graph_plan( + self, + state: StewardGraphState, + ) -> dict[str, StewardPlanResponse]: + return { + "plan": self._build_rule_fallback_plan( + state["request"], + base_date=state["base_date"], + model_call_traces=state.get("model_call_traces"), + fallback_reason=str(state.get("fallback_reason") or ""), + ) + } + + @staticmethod + def _attach_action_steps(state: StewardGraphState) -> dict[str, StewardPlanResponse]: + plan = state.get("plan") + if not isinstance(plan, StewardPlanResponse): + raise RuntimeError("LangGraph 小财管家动作规划缺少有效计划。") + return {"plan": StewardActionPlanBuilder().attach_action_steps(plan)} + + def _last_intent_call_traces( + self, + fallback_traces: list[dict[str, Any]], + ) -> list[dict[str, Any]]: + return getattr(self.intent_agent, "last_call_traces", []) or fallback_traces diff --git a/server/src/app/services/steward_graph_runtime.py b/server/src/app/services/steward_graph_runtime.py new file mode 100644 index 0000000..bafc36b --- /dev/null +++ b/server/src/app/services/steward_graph_runtime.py @@ -0,0 +1,214 @@ +from __future__ import annotations + +from typing import Any, TypedDict + +from langgraph.graph import END, START, StateGraph + +from app.schemas.steward import ( + StewardRuntimeDecisionRequest, + StewardRuntimeDecisionResponse, + StewardSlotDecisionRequest, + StewardSlotDecisionResponse, +) +from app.services.runtime_chat import RuntimeChatService +from app.services.steward_runtime_decision_agent import StewardRuntimeDecisionAgent +from app.services.steward_slot_decision_agent import StewardSlotDecisionAgent + + +class StewardSlotGraphState(TypedDict, total=False): + request: StewardSlotDecisionRequest + normalized_request: StewardSlotDecisionRequest + decision: StewardSlotDecisionResponse + model_call_traces: list[dict[str, Any]] + fallback_reason: str + + +class StewardRuntimeGraphState(TypedDict, total=False): + request: StewardRuntimeDecisionRequest + normalized_request: StewardRuntimeDecisionRequest + action_decision: StewardRuntimeDecisionResponse + decision: StewardRuntimeDecisionResponse + model_call_traces: list[dict[str, Any]] + fallback_reason: str + + +class StewardGraphRuntime: + """用 LangGraph 编排会话内槽位、记忆和行动决策。""" + + def __init__(self, runtime_chat_service: RuntimeChatService) -> None: + self.slot_agent = StewardSlotDecisionAgent(runtime_chat_service) + self.runtime_agent = StewardRuntimeDecisionAgent(runtime_chat_service) + self._slot_graph = self._build_slot_graph() + self._runtime_graph = self._build_runtime_graph() + + def decide_slot(self, request: StewardSlotDecisionRequest) -> StewardSlotDecisionResponse: + final_state = self._slot_graph.invoke( + { + "request": request, + "model_call_traces": [], + "fallback_reason": "", + } + ) + decision = final_state.get("decision") + if not isinstance(decision, StewardSlotDecisionResponse): + raise RuntimeError("LangGraph 槽位决策未生成有效结果。") + return decision + + def decide_runtime(self, request: StewardRuntimeDecisionRequest) -> StewardRuntimeDecisionResponse: + final_state = self._runtime_graph.invoke( + { + "request": request, + "model_call_traces": [], + "fallback_reason": "", + } + ) + decision = final_state.get("decision") + if not isinstance(decision, StewardRuntimeDecisionResponse): + raise RuntimeError("LangGraph 运行时决策未生成有效结果。") + return decision + + def _build_slot_graph(self): + graph = StateGraph(StewardSlotGraphState) + graph.add_node("slot_prepare_context", self._slot_prepare_context) + graph.add_node("slot_tool_decision", self._slot_tool_decision) + graph.add_node("slot_rule_fallback", self._slot_rule_fallback) + + graph.add_edge(START, "slot_prepare_context") + graph.add_edge("slot_prepare_context", "slot_tool_decision") + graph.add_conditional_edges( + "slot_tool_decision", + self._route_after_slot_tool_decision, + { + "done": END, + "fallback": "slot_rule_fallback", + }, + ) + graph.add_edge("slot_rule_fallback", END) + return graph.compile() + + def _build_runtime_graph(self): + graph = StateGraph(StewardRuntimeGraphState) + graph.add_node("runtime_memory_context", self._runtime_memory_context) + graph.add_node("runtime_action_decision", self._runtime_action_decision) + graph.add_node("runtime_tool_decision", self._runtime_tool_decision) + graph.add_node("runtime_rule_fallback", self._runtime_rule_fallback) + + graph.add_edge(START, "runtime_memory_context") + graph.add_conditional_edges( + "runtime_memory_context", + self._route_after_runtime_memory_context, + { + "action": "runtime_action_decision", + "tool": "runtime_tool_decision", + }, + ) + graph.add_edge("runtime_action_decision", END) + graph.add_conditional_edges( + "runtime_tool_decision", + self._route_after_runtime_tool_decision, + { + "done": END, + "fallback": "runtime_rule_fallback", + }, + ) + graph.add_edge("runtime_rule_fallback", END) + return graph.compile() + + def _slot_prepare_context(self, state: StewardSlotGraphState) -> dict[str, Any]: + return { + "normalized_request": self.slot_agent._normalize_request(state["request"]), + } + + def _slot_tool_decision(self, state: StewardSlotGraphState) -> dict[str, Any]: + request = state.get("normalized_request") or state["request"] + try: + return {"decision": self.slot_agent.decide(request)} + except Exception as exc: + return { + "model_call_traces": [ + *state.get("model_call_traces", []), + self._build_failure_trace("langgraph_slot_decision", exc), + ], + "fallback_reason": f"LangGraph 槽位工具节点失败,已切换规则兜底:{exc}", + } + + @staticmethod + def _route_after_slot_tool_decision(state: StewardSlotGraphState) -> str: + if isinstance(state.get("decision"), StewardSlotDecisionResponse): + return "done" + return "fallback" + + def _slot_rule_fallback(self, state: StewardSlotGraphState) -> dict[str, StewardSlotDecisionResponse]: + request = state.get("normalized_request") or self.slot_agent._normalize_request(state["request"]) + return { + "decision": self.slot_agent._build_rule_fallback( + request, + state.get("model_call_traces", []), + ) + } + + def _runtime_memory_context(self, state: StewardRuntimeGraphState) -> dict[str, Any]: + request = self.runtime_agent._normalize_request(state["request"]) + action_decision = self.runtime_agent._build_selected_flow_decision(request, []) + update: dict[str, Any] = {"normalized_request": request} + if action_decision is not None: + update["action_decision"] = action_decision + return update + + @staticmethod + def _route_after_runtime_memory_context(state: StewardRuntimeGraphState) -> str: + if isinstance(state.get("action_decision"), StewardRuntimeDecisionResponse): + return "action" + return "tool" + + def _runtime_action_decision( + self, + state: StewardRuntimeGraphState, + ) -> dict[str, StewardRuntimeDecisionResponse]: + return {"decision": state["action_decision"]} + + def _runtime_tool_decision(self, state: StewardRuntimeGraphState) -> dict[str, Any]: + request = state.get("normalized_request") or state["request"] + try: + return {"decision": self.runtime_agent.decide(request)} + except Exception as exc: + return { + "model_call_traces": [ + *state.get("model_call_traces", []), + self._build_failure_trace("langgraph_runtime_decision", exc), + ], + "fallback_reason": f"LangGraph 运行时工具节点失败,已切换规则兜底:{exc}", + } + + @staticmethod + def _route_after_runtime_tool_decision(state: StewardRuntimeGraphState) -> str: + if isinstance(state.get("decision"), StewardRuntimeDecisionResponse): + return "done" + return "fallback" + + def _runtime_rule_fallback( + self, + state: StewardRuntimeGraphState, + ) -> dict[str, StewardRuntimeDecisionResponse]: + request = state.get("normalized_request") or self.runtime_agent._normalize_request(state["request"]) + decision = self.runtime_agent._build_rule_fallback( + request, + state.get("model_call_traces", []), + ) + return { + "decision": self.runtime_agent._attach_updated_steward_state( + decision, + request, + ) + } + + @staticmethod + def _build_failure_trace(slot: str, exc: Exception) -> dict[str, Any]: + return { + "slot": slot, + "provider": "langgraph", + "model": "", + "attempt": 1, + "status": "failed", + "error": str(exc), + } diff --git a/server/src/app/services/steward_intent_agent.py b/server/src/app/services/steward_intent_agent.py index aae8b2e..c82d67d 100644 --- a/server/src/app/services/steward_intent_agent.py +++ b/server/src/app/services/steward_intent_agent.py @@ -42,8 +42,9 @@ class StewardIntentAgent: }, max_tokens=1800, temperature=0.1, - timeout_seconds=45, - max_attempts=1, + timeout_seconds=10, + max_attempts=3, + use_failure_cooldown=False, ) self.last_call_traces = result.calls_as_dicts() if result.tool_call is None or result.tool_call.name != STEWARD_INTENT_FUNCTION_NAME: @@ -113,6 +114,8 @@ class StewardIntentAgent: "candidate_flows 同时给出 travel_application 和 travel_reimbursement,tasks 保持空数组。" "所有 ontology_fields 只能使用调用方给出的 canonical_ontology_fields;" "如果输入里出现 occurred_date、transport_type、reason_value 等别名,必须映射为 canonical 字段。" + "每个 task 必须输出 requested_action:用户只是要求整理/发起但未说保存或提交时为 preview;" + "用户说保存草稿、先保存、存草稿时为 save_draft;用户说直接提交、提交申请、确认提交时为 submit。" "相对日期必须以 base_date 为准转换为明确日期。" "thinking_events 只能是面向用户的过程摘要,不能暴露内部推理链。" "如果用户输入与出差、费用、报销、申请等财务事项完全无关" @@ -165,6 +168,10 @@ class StewardIntentAgent: "minimum": 0, "maximum": 1, }, + "requested_action": { + "type": "string", + "enum": ["preview", "save_draft", "submit"], + }, "ontology_fields": { "type": "object", "additionalProperties": {"type": "string"}, @@ -182,6 +189,7 @@ class StewardIntentAgent: "title", "summary", "confidence", + "requested_action", "ontology_fields", "missing_fields", ], diff --git a/server/src/app/services/steward_model_plan_builder.py b/server/src/app/services/steward_model_plan_builder.py index da95c35..b6c9aee 100644 --- a/server/src/app/services/steward_model_plan_builder.py +++ b/server/src/app/services/steward_model_plan_builder.py @@ -157,6 +157,10 @@ class StewardModelPlanBuilder: task_type=task_type, fields=fields, ) + requested_action = self._sanitize_requested_action( + raw_task.get("requested_action"), + fallback_text=request.message, + ) tasks.append( StewardTask( task_id=task_id, @@ -171,6 +175,7 @@ class StewardModelPlanBuilder: fields=fields, task_type=task_type, ), + requested_action=requested_action, # type: ignore[arg-type] ontology_fields=fields, missing_fields=missing_fields, confirmation_required=True, @@ -441,6 +446,17 @@ class StewardModelPlanBuilder: missing_fields.append(key) return missing_fields + def _sanitize_requested_action(self, raw_action: Any, *, fallback_text: str = "") -> str: + action = self.planner._clean_text(raw_action) + if action in {"preview", "save_draft", "submit"}: + return action + compact = self.planner._clean_text(fallback_text).replace(" ", "") + if any(keyword in compact for keyword in ("直接提交", "提交申请", "确认提交", "提交审批")): + return "submit" + if any(keyword in compact for keyword in ("保存草稿", "存草稿", "先保存")): + return "save_draft" + return "preview" + def _resolve_model_confidence( self, value: Any, @@ -459,6 +475,8 @@ class StewardModelPlanBuilder: if not cleaned: return "" if key == "time_range": + if re.search(r"(?:至|到|~|~|—|--|-).*\d", cleaned): + return cleaned return self.planner._extract_time_range(cleaned, base_date) or cleaned if key == "expense_type": return self._normalize_expense_type_value(cleaned) diff --git a/server/src/app/services/steward_planner.py b/server/src/app/services/steward_planner.py index 10aa1fb..4b44092 100644 --- a/server/src/app/services/steward_planner.py +++ b/server/src/app/services/steward_planner.py @@ -3,6 +3,7 @@ from __future__ import annotations from typing import Any from app.schemas.steward import StewardPlanRequest, StewardPlanResponse +from app.services.steward_action_contracts import StewardActionPlanBuilder from app.services.steward_constants import BUSINESS_CANONICAL_FIELD_ORDER from app.services.steward_intent_agent import StewardIntentAgent from app.services.steward_model_plan_builder import StewardModelPlanBuilder @@ -28,10 +29,7 @@ class StewardPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtractio raise ValueError("小财管家需要一段任务描述。") base_date = self._resolve_base_date(request.client_now_iso, request.context_json) - # 业务无关输入拦截(纯数字、问候、闲聊、乱码等):在进入 LLM/规则兜底之前直接返回 off_topic 计划。 scenario = self._classify_irrelevant_input(message, request) - if scenario is not None: - return self._build_off_topic_plan(request, scenario=scenario) model_call_traces: list[dict[str, Any]] = [] fallback_reason = "" if self.intent_agent is not None and self._should_use_model_intent_recognition(message, base_date, request): @@ -50,26 +48,44 @@ class StewardPlannerService(StewardPlannerFallbackMixin, StewardPlannerExtractio ) if llm_plan is not None: if self._looks_like_ambiguous_travel_flow(message, base_date, request): - return self._build_pending_flow_fallback_plan( - request, - base_date=base_date, - model_call_traces=model_call_traces, - fallback_reason=( - "主模型返回了直接任务,但当前话术没有明确申请或报销动作;" - "服务端已改为候选流程确认,避免误入申请流程。" - ), - planning_source="llm_function_call", + return self._with_action_steps( + self._build_pending_flow_fallback_plan( + request, + base_date=base_date, + model_call_traces=model_call_traces, + fallback_reason=( + "主模型返回了直接任务,但当前话术没有明确申请或报销动作;" + "服务端已改为候选流程确认,避免误入申请流程。" + ), + planning_source="llm_function_call", + ) ) - return llm_plan + return self._with_action_steps(llm_plan) model_call_traces = getattr(self.intent_agent, "last_call_traces", []) or model_call_traces fallback_reason = "主模型未返回可用的 function calling 计划,已切换到规则兜底。" except Exception as exc: model_call_traces = getattr(self.intent_agent, "last_call_traces", []) or model_call_traces fallback_reason = f"主模型 function calling 调用失败,已切换到规则兜底:{exc}" - return self._build_rule_fallback_plan( - request, - base_date=base_date, - model_call_traces=model_call_traces, - fallback_reason=fallback_reason, + if scenario is not None: + return self._with_action_steps( + self._build_off_topic_plan( + request, + scenario=scenario, + model_call_traces=model_call_traces, + fallback_reason=fallback_reason, + ) + ) + + return self._with_action_steps( + self._build_rule_fallback_plan( + request, + base_date=base_date, + model_call_traces=model_call_traces, + fallback_reason=fallback_reason, + ) ) + + @staticmethod + def _with_action_steps(plan: StewardPlanResponse) -> StewardPlanResponse: + return StewardActionPlanBuilder().attach_action_steps(plan) diff --git a/server/src/app/services/steward_planner_extraction.py b/server/src/app/services/steward_planner_extraction.py index e0120a2..ac90bb8 100644 --- a/server/src/app/services/steward_planner_extraction.py +++ b/server/src/app/services/steward_planner_extraction.py @@ -133,6 +133,7 @@ class StewardPlannerExtractionMixin: summary=self._build_task_summary(draft.segment, fields), status="needs_confirmation", confidence=self._resolve_task_confidence(draft.segment, fields, draft.task_type), + requested_action=self._resolve_requested_action(draft.segment), # type: ignore[arg-type] ontology_fields=fields, missing_fields=missing_fields, confirmation_required=True, @@ -205,6 +206,15 @@ class StewardPlannerExtractionMixin: def _extract_transport_mode(segment: str) -> str: return ApplicationFactResolver.extract_transport_mode(segment) + @staticmethod + def _resolve_requested_action(segment: str) -> str: + compact = re.sub(r"\s+", "", segment) + if re.search(r"直接提交|提交申请|确认提交|提交审批", compact): + return "submit" + if re.search(r"保存草稿|存草稿|先保存|保存", compact): + return "save_draft" + return "preview" + @staticmethod def _resolve_missing_fields(task_type: str, fields: dict[str, str]) -> list[str]: required = ["expense_type", "time_range", "reason"] @@ -575,4 +585,3 @@ class StewardPlannerExtractionMixin: @staticmethod def _clean_text(value: Any) -> str: return re.sub(r"\s+", " ", str(value or "")).strip() - diff --git a/server/src/app/services/steward_planner_fallback.py b/server/src/app/services/steward_planner_fallback.py index ab057af..4051a93 100644 --- a/server/src/app/services/steward_planner_fallback.py +++ b/server/src/app/services/steward_planner_fallback.py @@ -34,9 +34,8 @@ class StewardPlannerFallbackMixin: base_date: date, request: StewardPlanRequest, ) -> bool: - if self._looks_like_ambiguous_travel_flow(message, base_date, request): - return False - return self._has_multiple_financial_demands(message) + return bool(message.strip()) + @staticmethod def _is_business_irrelevant_input(message: str, request: StewardPlanRequest) -> bool: @@ -105,12 +104,14 @@ class StewardPlannerFallbackMixin: request: StewardPlanRequest, *, scenario: str, + model_call_traces: list[dict[str, Any]] | None = None, + fallback_reason: str = "", ) -> StewardPlanResponse: """业务无关输入的兜底计划:根据场景给出对应引导,off_business 场景可由 LLM 增强。""" base_summary = self._default_off_topic_summary(scenario) thinking_event = self._build_off_topic_thinking_event(scenario) suggested_prompts = self._off_topic_suggested_prompts(scenario) - model_call_traces: list[dict[str, Any]] = [] + traces = list(model_call_traces or []) # 仅对 off_business 场景尝试让 LLM 生成多样化引导;问候/无意义场景用规则模板即可。 if ( @@ -121,24 +122,36 @@ class StewardPlannerFallbackMixin: llm_result = self.off_topic_agent.generate(request, scenario=scenario) if llm_result is not None and llm_result.response_text: base_summary = llm_result.response_text - model_call_traces = llm_result.model_call_traces + traces = llm_result.model_call_traces except Exception: # 失败时静默回退到规则模板 pass + thinking_events = [thinking_event] + if fallback_reason: + thinking_events.insert( + 0, + StewardThinkingEvent( + event_id="intent_agent_rule_fallback", + stage="rule_fallback", + title="意图识别智能体进入兜底模式", + content=fallback_reason, + ), + ) + return StewardPlanResponse( plan_id=f"steward_plan_{uuid.uuid4().hex[:12]}", plan_status="off_topic", planning_source="rule_fallback", next_action="none", summary=base_summary, - thinking_events=[thinking_event], + thinking_events=thinking_events, tasks=[], attachment_groups=[], confirmation_groups=[], candidate_flows=[], suggested_prompts=suggested_prompts, - model_call_traces=model_call_traces, + model_call_traces=traces, ) @staticmethod @@ -435,4 +448,3 @@ class StewardPlannerFallbackMixin: ) return drafts - diff --git a/server/tests/test_application_fact_resolver.py b/server/tests/test_application_fact_resolver.py index 4248ea7..b6fea72 100644 --- a/server/tests/test_application_fact_resolver.py +++ b/server/tests/test_application_fact_resolver.py @@ -22,6 +22,17 @@ def test_application_fact_resolver_extracts_travel_application_fields() -> None: assert facts["transport_mode"] == "train" +def test_application_fact_resolver_drops_transport_prompt_from_application_reason() -> None: + facts = resolve_application_facts( + "2026-02-20 至 2026-02-23,去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交", + "expense_application", + date(2026, 6, 24), + ) + + assert facts["reason"] == "辅助国网仿生产服务器部署" + assert facts["transport_mode"] == "train" + + def test_application_fact_resolver_preserves_reimbursement_transport_semantics() -> None: facts = resolve_application_facts( "报销昨天去北京客户现场沟通产生的出租车费用", diff --git a/server/tests/test_reimbursement_endpoints.py b/server/tests/test_reimbursement_endpoints.py index f7b79f2..7c03a69 100644 --- a/server/tests/test_reimbursement_endpoints.py +++ b/server/tests/test_reimbursement_endpoints.py @@ -939,7 +939,7 @@ def test_application_preview_action_submits_without_orchestrator_run(monkeypatch assert draft_payload["draft_type"] == "expense_application" assert draft_payload["status"] == "submitted" assert draft_payload["approval_stage"] == "直属领导审批" - assert draft_payload["claim_no"].startswith("AP-") + assert draft_payload["claim_no"].startswith("A") with session_factory() as db: claim = db.get(ExpenseClaim, draft_payload["claim_id"]) @@ -1015,7 +1015,7 @@ def test_application_preview_action_saves_draft_with_detail_reference(monkeypatc assert draft_payload["status"] == "draft" assert draft_payload["approval_stage"] == "待提交" assert draft_payload["claim_id"] - assert draft_payload["claim_no"].startswith("AP-") + assert draft_payload["claim_no"].startswith("A") with session_factory() as db: claim = db.get(ExpenseClaim, draft_payload["claim_id"]) diff --git a/server/tests/test_runtime_chat_service.py b/server/tests/test_runtime_chat_service.py index 60ceb3f..7179e3f 100644 --- a/server/tests/test_runtime_chat_service.py +++ b/server/tests/test_runtime_chat_service.py @@ -242,6 +242,50 @@ def test_runtime_chat_supports_single_pass_fast_failover(monkeypatch) -> None: assert calls == [("main", 8), ("backup", 20)] +def test_runtime_chat_complete_with_tool_call_fails_over_to_backup_before_retrying_main(monkeypatch) -> None: + _clear_runtime_chat_cooldown() + session_factory = build_session_factory() + with session_factory() as db: + service = RuntimeChatService(db) + calls: list[str] = [] + + def fake_load_chat_slot(slot: str): + return { + "slot": slot, + "provider": "MiniMax" if slot == "main" else "GLM", + "endpoint": "https://example.com/v1", + "model": "main-model" if slot == "main" else "backup-model", + "apiKey": "secret", + } + + def fake_request_chat_tool_call(config, messages, *, tools, tool_choice, max_tokens, temperature, timeout_seconds): + del messages, tools, tool_choice, max_tokens, temperature, timeout_seconds + calls.append(config["slot"]) + if config["slot"] == "main": + raise RuntimeError("main tool call unavailable") + return runtime_chat_module.RuntimeChatToolCall( + name="submit_steward_intent_plan", + arguments={"tasks": [{"task_type": "expense_application"}]}, + ) + + monkeypatch.setattr(service, "_load_chat_slot", fake_load_chat_slot) + monkeypatch.setattr(service, "_request_chat_tool_call", fake_request_chat_tool_call) + + result = service.complete_with_tool_call( + [{"role": "user", "content": "保存草稿"}], + tools=[{"type": "function", "function": {"name": "submit_steward_intent_plan"}}], + tool_choice={"type": "function", "function": {"name": "submit_steward_intent_plan"}}, + max_attempts=3, + use_failure_cooldown=False, + ) + + assert result.tool_call is not None + assert result.tool_call.name == "submit_steward_intent_plan" + assert result.tool_call.arguments["tasks"][0]["task_type"] == "expense_application" + assert calls == ["main", "backup"] + assert [item.status for item in result.calls] == ["failed", "succeeded"] + + def test_runtime_chat_skips_slot_during_cooldown(monkeypatch) -> None: _clear_runtime_chat_cooldown() session_factory = build_session_factory() @@ -271,3 +315,51 @@ def test_runtime_chat_skips_slot_during_cooldown(monkeypatch) -> None: assert service.complete([{"role": "user", "content": "hello"}], max_attempts=1) == "backup answer" assert service.complete([{"role": "user", "content": "hello again"}], max_attempts=1) == "backup answer" assert calls == ["main", "backup", "backup"] + + +def test_runtime_chat_tool_call_can_retry_without_failure_cooldown(monkeypatch) -> None: + _clear_runtime_chat_cooldown() + session_factory = build_session_factory() + with session_factory() as db: + service = RuntimeChatService(db) + calls: list[str] = [] + + def fake_load_chat_slot(slot: str): + return { + "slot": slot, + "provider": slot, + "endpoint": "https://example.com/v1", + "model": f"{slot}-model", + "apiKey": "secret", + } + + def fake_request_chat_tool_call( + config, + messages, + *, + tools, + tool_choice, + max_tokens, + temperature, + timeout_seconds, + ): + del messages, tools, tool_choice, max_tokens, temperature, timeout_seconds + calls.append(config["slot"]) + raise RuntimeError("tool call timeout") + + monkeypatch.setattr(service, "_load_chat_slot", fake_load_chat_slot) + monkeypatch.setattr(service, "_request_chat_tool_call", fake_request_chat_tool_call) + monkeypatch.setattr("app.services.runtime_chat.sleep", lambda *_args, **_kwargs: None) + + result = service.complete_with_tool_call( + [{"role": "user", "content": "hello"}], + tools=[{"type": "function", "function": {"name": "submit_steward_intent_plan"}}], + tool_choice={"type": "function", "function": {"name": "submit_steward_intent_plan"}}, + slot_priority=("main",), + max_attempts=3, + use_failure_cooldown=False, + ) + + assert result.tool_call is None + assert calls == ["main", "main", "main"] + assert [item.status for item in result.calls] == ["failed", "failed", "failed"] diff --git a/server/tests/test_steward_action_executor.py b/server/tests/test_steward_action_executor.py new file mode 100644 index 0000000..f2ceb50 --- /dev/null +++ b/server/tests/test_steward_action_executor.py @@ -0,0 +1,455 @@ +from __future__ import annotations + +from collections.abc import Generator +from datetime import UTC, datetime + +from fastapi.testclient import TestClient +from sqlalchemy import create_engine, select +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import StaticPool + +from app.api.deps import get_db +from app.db.base import Base +from app.main import create_app +from app.models.agent_conversation import AgentConversation +from app.models.employee import Employee +from app.models.financial_record import ExpenseClaim +from app.services import attachment_association_jobs as attachment_jobs_module + + +def build_session_factory() -> sessionmaker[Session]: + engine = create_engine( + "sqlite+pysqlite:///:memory:", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all(bind=engine) + return sessionmaker(bind=engine, autoflush=False, autocommit=False) + + +def build_client() -> tuple[TestClient, sessionmaker[Session]]: + session_factory = build_session_factory() + app = create_app() + + def override_db() -> Generator[Session, None, None]: + db = session_factory() + try: + yield db + finally: + db.close() + + app.dependency_overrides[get_db] = override_db + return TestClient(app), session_factory + + +def seed_employee(db: Session) -> None: + manager = Employee( + id="steward-action-manager", + employee_no="E90000", + name="李总", + email="leader@example.com", + position="部门负责人", + grade="P7", + ) + employee = Employee( + id="steward-action-employee", + employee_no="E90001", + name="张三", + email="zhangsan@example.com", + position="实施工程师", + grade="P4", + manager=manager, + ) + db.add_all([manager, employee]) + db.commit() + + +def auth_headers() -> dict[str, str]: + return { + "x-auth-username": "zhangsan@example.com", + "x-auth-name": "Zhang San", + "x-auth-employee-no": "E90001", + "x-auth-role-codes": "user", + "x-auth-position": "Engineer", + "x-auth-grade": "P4", + "x-auth-manager-name": "Leader", + } + + +def base_application_task(requested_action: str = "save_draft") -> dict[str, object]: + return { + "task_id": "task_app_001", + "task_type": "expense_application", + "assigned_agent": "application_assistant", + "title": "上海出差申请", + "summary": "2026-02-20 至 2026-02-23 去上海出差,辅助国网仿生产服务器部署,火车出行。", + "status": "needs_confirmation", + "confidence": 0.96, + "requested_action": requested_action, + "ontology_fields": { + "expense_type": "travel", + "time_range": "2026-02-20 至 2026-02-23", + "location": "上海", + "reason": "辅助国网仿生产服务器部署", + "transport_mode": "train", + }, + "missing_fields": [], + "confirmation_required": requested_action == "submit", + "action_steps": [], + } + + +def base_reimbursement_task() -> dict[str, object]: + return { + "task_id": "task_reim_001", + "task_type": "reimbursement", + "assigned_agent": "reimbursement_assistant", + "title": "客户现场交通费报销", + "summary": "2026-03-04 打车去客户现场,交通费 32 元。", + "status": "needs_confirmation", + "confidence": 0.9, + "requested_action": "save_draft", + "ontology_fields": { + "expense_type": "transport", + "time_range": "2026-03-04", + "location": "客户现场", + "reason": "客户现场沟通", + "amount": "32元", + "transport_mode": "taxi", + }, + "missing_fields": [], + "confirmation_required": False, + "action_steps": [], + } + + +def claim_count(db: Session) -> int: + return len(db.scalars(select(ExpenseClaim)).all()) + + +def seed_approved_application(db: Session) -> None: + application = ExpenseClaim( + id="application-action-approved", + claim_no="AAPPROVED1", + employee_id="steward-action-employee", + employee_name="张三", + department_id="dept-delivery", + department_name="交付部", + project_code=None, + expense_type="travel_application", + reason="辅助国网仿生产服务器部署", + location="上海", + amount=3000, + currency="CNY", + invoice_count=0, + occurred_at=datetime(2026, 2, 20, tzinfo=UTC), + submitted_at=None, + status="approved", + approval_stage="已完成", + risk_flags_json=[], + ) + db.add(application) + db.commit() + + +def test_steward_action_executor_rejects_unknown_action_without_creating_claim() -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + before_count = claim_count(db) + + response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json={ + "action_type": "delete_all_claims", + "message": "请执行未知动作", + "task": base_application_task(), + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "blocked" + assert payload["action_type"] == "delete_all_claims" + assert "不支持" in payload["message"] + with session_factory() as db: + assert claim_count(db) == before_count + + +def test_steward_action_executor_blocks_attachment_action_without_receipts() -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + before_count = claim_count(db) + + task = base_reimbursement_task() + task["ontology_fields"] = { + **task["ontology_fields"], + "attachments": "taxi.png", + } + response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json={ + "action_type": "associate_attachments", + "message": "关联附件 taxi.png", + "task": task, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "blocked" + assert "receipt_id" in payload["message"] or "票据" in payload["message"] + with session_factory() as db: + assert claim_count(db) == before_count + + +def test_steward_action_executor_records_pending_interrupt_in_conversation_state() -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + + response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json={ + "action_type": "submit_application", + "message": "2026-02-20 至 2026-02-23,去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交", + "conversation_id": "conv-action-submit", + "client_trace_id": "trace-submit-pending", + "task": base_application_task("submit"), + "confirmed": False, + "context_json": { + "precheck_result": { + "status": "ok", + "blocking": False, + } + }, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "needs_confirmation" + with session_factory() as db: + conversation = db.scalar( + select(AgentConversation).where( + AgentConversation.conversation_id == "conv-action-submit" + ) + ) + assert conversation is not None + checkpoint = conversation.state_json["steward_action_checkpoint"] + assert checkpoint["pending_interrupt"]["client_trace_id"] == "trace-submit-pending" + assert checkpoint["pending_interrupt"]["action_type"] == "submit_application" + assert checkpoint["actions"]["trace-submit-pending"]["status"] == "needs_confirmation" + + +def test_steward_action_executor_reuses_checkpoint_for_duplicate_trace_without_duplicate_draft() -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + + request_payload = { + "action_type": "save_application_draft", + "message": "2026-02-20 至 2026-02-23,去上海出差,辅助国网仿生产服务器部署,交通火车,保存草稿", + "conversation_id": "conv-action-draft", + "client_trace_id": "trace-save-draft", + "task": base_application_task("save_draft"), + } + first_response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json=request_payload, + ) + second_response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json=request_payload, + ) + + assert first_response.status_code == 200 + assert second_response.status_code == 200 + first_payload = first_response.json() + second_payload = second_response.json() + assert first_payload["status"] == "succeeded" + assert second_payload["status"] == "succeeded" + assert ( + first_payload["result_payload"]["draft_payload"]["claim_id"] + == second_payload["result_payload"]["draft_payload"]["claim_id"] + ) + assert second_payload["result_payload"]["idempotent_replay"] is True + with session_factory() as db: + assert claim_count(db) == 1 + + +def test_steward_action_executor_requires_confirmation_before_submit_side_effect() -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + + response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json={ + "action_type": "submit_application", + "message": "2026-02-20 至 2026-02-23,去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交", + "task": base_application_task("submit"), + "confirmed": False, + "context_json": { + "precheck_result": { + "status": "ok", + "blocking": False, + } + }, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "needs_confirmation" + assert payload["requires_confirmation"] is True + with session_factory() as db: + assert claim_count(db) == 0 + + +def test_steward_action_executor_saves_application_draft_from_action_step() -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + + response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json={ + "action_type": "save_application_draft", + "message": "2026-02-20 至 2026-02-23,去上海出差,辅助国网仿生产服务器部署,交通火车,保存草稿", + "task": base_application_task("save_draft"), + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "succeeded" + draft_payload = payload["result_payload"]["draft_payload"] + assert draft_payload["draft_type"] == "expense_application" + assert draft_payload["status"] == "draft" + assert draft_payload["claim_no"].startswith("A") + with session_factory() as db: + claim = db.scalars(select(ExpenseClaim)).one() + assert claim.status == "draft" + assert claim.reason == "辅助国网仿生产服务器部署" + + +def test_steward_action_executor_creates_reimbursement_draft_from_action_step() -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + + response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json={ + "action_type": "create_reimbursement_draft", + "message": "2026-03-04,打车去客户现场,交通费32元,保存草稿", + "task": base_reimbursement_task(), + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "succeeded" + assert payload["result_payload"]["status"] == "draft" + assert payload["result_payload"]["claim_id"] + with session_factory() as db: + claim = db.scalars(select(ExpenseClaim)).one() + assert claim.status == "draft" + assert claim.expense_type == "transport" + assert claim.reason == "客户现场沟通" + + +def test_steward_action_executor_links_application_when_creating_reimbursement_draft() -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + seed_approved_application(db) + + response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json={ + "action_type": "link_existing_application", + "message": "关联申请单 AAPPROVED1,并保存报销草稿", + "task": base_reimbursement_task(), + "context_json": { + "application_claim_id": "application-action-approved", + "application_claim_no": "AAPPROVED1", + "application_reason": "辅助国网仿生产服务器部署", + "application_location": "上海", + "application_business_time": "2026-02-20 至 2026-02-23", + }, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "succeeded" + assert payload["result_payload"]["status"] == "draft" + with session_factory() as db: + claims = db.scalars(select(ExpenseClaim)).all() + reimbursement = next(claim for claim in claims if claim.id != "application-action-approved") + assert reimbursement.status == "draft" + link_flags = [ + flag + for flag in list(reimbursement.risk_flags_json or []) + if isinstance(flag, dict) and flag.get("source") == "application_link" + ] + assert link_flags + assert link_flags[0]["application_claim_no"] == "AAPPROVED1" + + +def test_steward_action_executor_associates_receipt_attachments(monkeypatch) -> None: + client, session_factory = build_client() + with session_factory() as db: + seed_employee(db) + + calls: list[dict[str, object]] = [] + + def fake_run(self, *, receipt_ids, current_user): + calls.append({ + "receipt_ids": list(receipt_ids), + "username": current_user.username, + }) + return { + "claim_id": "claim-associated", + "claim_no": "BX-20260220-001", + "uploaded_count": 2, + "skipped_count": 0, + } + + monkeypatch.setattr(attachment_jobs_module.AttachmentAssociationJobRunner, "run", fake_run) + + response = client.post( + "/api/v1/steward/actions/execute", + headers=auth_headers(), + json={ + "action_type": "associate_attachments", + "message": "把两张火车票关联到报销草稿", + "task": base_reimbursement_task(), + "context_json": { + "receipt_ids": ["receipt-001", "receipt-002"], + }, + }, + ) + + assert response.status_code == 200 + payload = response.json() + assert payload["status"] == "succeeded" + assert payload["result_payload"]["claim_no"] == "BX-20260220-001" + assert calls == [ + { + "receipt_ids": ["receipt-001", "receipt-002"], + "username": "zhangsan@example.com", + } + ] diff --git a/server/tests/test_steward_graph_planner.py b/server/tests/test_steward_graph_planner.py new file mode 100644 index 0000000..219d5e8 --- /dev/null +++ b/server/tests/test_steward_graph_planner.py @@ -0,0 +1,234 @@ +from __future__ import annotations + +from app.api.v1.endpoints import steward as steward_endpoint +from app.core.config import get_settings +from app.schemas.steward import StewardPlanRequest +from app.services.steward_graph_planner import StewardGraphPlannerService +from app.services.steward_intent_agent import StewardIntentAgentResult +from app.services.steward_planner import StewardPlannerService + + +class GraphTravelApplicationIntentAgent: + def __init__(self) -> None: + self.calls = 0 + + def detect(self, request, *, base_date, canonical_fields): + self.calls += 1 + return StewardIntentAgentResult( + payload={ + "thinking_events": [ + { + "stage": "task_split", + "title": "识别出差申请草稿", + "content": "模型识别到用户要创建上海出差申请,并保存草稿。", + } + ], + "tasks": [ + { + "task_type": "expense_application", + "title": "上海出差申请", + "summary": ( + "2026-02-20 至 2026-02-23 前往上海," + "国网仿生产服务器部署,火车出行。" + ), + "requested_action": "save_draft", + "confidence": 0.95, + "ontology_fields": { + "time_range": "2026-02-20 至 2026-02-23", + "location": "上海", + "expense_type": "差旅", + "reason": "国网仿生产服务器部署", + "transport_type": "火车", + }, + "missing_fields": [], + } + ], + "attachment_groups": [], + }, + model_call_traces=[ + { + "slot": "main", + "provider": "MiniMax", + "model": "abab-test", + "attempt": 1, + "status": "succeeded", + } + ], + ) + + +class GraphSubmitTravelApplicationIntentAgent: + def __init__(self) -> None: + self.calls = 0 + + def detect(self, request, *, base_date, canonical_fields): + self.calls += 1 + return StewardIntentAgentResult( + payload={ + "thinking_events": [ + { + "stage": "task_split", + "title": "识别出差申请提交", + "content": "模型识别到用户要创建上海出差申请,并直接提交。", + } + ], + "tasks": [ + { + "task_type": "expense_application", + "title": "上海出差申请", + "summary": ( + "2026-02-20 至 2026-02-23 前往上海," + "辅助国网仿生产服务器部署,火车出行。" + ), + "requested_action": "submit", + "confidence": 0.96, + "ontology_fields": { + "time_range": "2026-02-20 至 2026-02-23", + "location": "上海", + "expense_type": "差旅", + "reason": "辅助国网仿生产服务器部署", + "transport_mode": "火车", + }, + "missing_fields": [], + } + ], + "attachment_groups": [], + }, + model_call_traces=[ + { + "slot": "main", + "provider": "MiniMax", + "model": "abab-test", + "attempt": 1, + "status": "succeeded", + } + ], + ) + + +class GraphEmptyIntentAgent: + def __init__(self) -> None: + self.calls = 0 + + def detect(self, request, *, base_date, canonical_fields): + self.calls += 1 + return None + + +def test_langgraph_planner_preserves_llm_save_draft_plan() -> None: + intent_agent = GraphTravelApplicationIntentAgent() + service = StewardGraphPlannerService(intent_agent=intent_agent) + + result = service.build_plan( + StewardPlanRequest( + message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署,火车,保存草稿", + client_now_iso="2026-02-10T09:00:00+08:00", + ) + ) + + assert intent_agent.calls == 1 + assert result.planning_source == "llm_function_call" + assert result.tasks[0].requested_action == "save_draft" + assert result.tasks[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23" + assert result.tasks[0].ontology_fields["transport_mode"] == "train" + assert result.model_call_traces[0]["provider"] == "MiniMax" + + +def test_langgraph_planner_builds_submit_action_steps_for_application() -> None: + intent_agent = GraphSubmitTravelApplicationIntentAgent() + service = StewardGraphPlannerService(intent_agent=intent_agent) + + result = service.build_plan( + StewardPlanRequest( + message="2026-02-20 至 2026-02-23,去上海出差,辅助国网仿生产服务器部署,交通火车,直接提交", + client_now_iso="2026-02-10T09:00:00+08:00", + ) + ) + + assert intent_agent.calls == 1 + assert result.planning_source == "llm_function_call" + assert result.action_steps[0].action_type == "detect_intent" + assert [step.action_type for step in result.tasks[0].action_steps] == [ + "fill_application_fields", + "build_application_preview", + "validate_required_fields", + "run_duplicate_precheck", + "submit_application", + ] + assert result.tasks[0].action_steps[0].payload["ontology_fields"]["location"] == "上海" + assert result.tasks[0].action_steps[-1].requires_confirmation is True + assert result.tasks[0].action_steps[-1].status == "pending_confirmation" + + +def test_langgraph_planner_falls_back_when_model_returns_no_tool_call() -> None: + intent_agent = GraphEmptyIntentAgent() + service = StewardGraphPlannerService(intent_agent=intent_agent) + + result = service.build_plan( + StewardPlanRequest( + message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署,火车,保存草稿", + client_now_iso="2026-02-10T09:00:00+08:00", + ) + ) + + assert intent_agent.calls == 1 + assert result.planning_source == "rule_fallback" + assert result.tasks[0].requested_action == "save_draft" + assert result.tasks[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23" + assert result.tasks[0].ontology_fields["transport_mode"] == "train" + assert result.model_call_traces == [] + + +def test_langgraph_planner_rule_fallback_builds_save_draft_action_steps() -> None: + intent_agent = GraphEmptyIntentAgent() + service = StewardGraphPlannerService(intent_agent=intent_agent) + + result = service.build_plan( + StewardPlanRequest( + message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署,火车,保存草稿", + client_now_iso="2026-02-10T09:00:00+08:00", + ) + ) + + assert result.planning_source == "rule_fallback" + assert result.tasks[0].requested_action == "save_draft" + assert [step.action_type for step in result.tasks[0].action_steps] == [ + "fill_application_fields", + "build_application_preview", + "validate_required_fields", + "save_application_draft", + ] + assert result.tasks[0].action_steps[-1].status == "planned" + + +def test_build_steward_planner_uses_langgraph_runtime_when_enabled(monkeypatch) -> None: + monkeypatch.setenv("STEWARD_AGENT_RUNTIME", "langgraph") + get_settings.cache_clear() + try: + planner = steward_endpoint._build_steward_planner(db=object()) + finally: + get_settings.cache_clear() + + assert isinstance(planner, StewardGraphPlannerService) + + +def test_build_steward_planner_defaults_to_langgraph_runtime(monkeypatch) -> None: + monkeypatch.delenv("STEWARD_AGENT_RUNTIME", raising=False) + get_settings.cache_clear() + try: + planner = steward_endpoint._build_steward_planner(db=object()) + finally: + get_settings.cache_clear() + + assert isinstance(planner, StewardGraphPlannerService) + + +def test_build_steward_planner_can_fall_back_to_legacy_runtime(monkeypatch) -> None: + monkeypatch.setenv("STEWARD_AGENT_RUNTIME", "legacy") + get_settings.cache_clear() + try: + planner = steward_endpoint._build_steward_planner(db=object()) + finally: + get_settings.cache_clear() + + assert isinstance(planner, StewardPlannerService) diff --git a/server/tests/test_steward_graph_runtime.py b/server/tests/test_steward_graph_runtime.py new file mode 100644 index 0000000..fb77ad5 --- /dev/null +++ b/server/tests/test_steward_graph_runtime.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +from typing import Any + +import pytest + +from app.api.v1.endpoints import steward as steward_endpoint +from app.core.config import get_settings +from app.schemas.steward import ( + StewardRuntimeDecisionRequest, + StewardSlotDecisionRequest, +) +from app.services.runtime_chat import ( + RuntimeChatCallTrace, + RuntimeChatToolCall, + RuntimeToolCallResult, +) +from app.services.steward_graph_runtime import StewardGraphRuntime +from app.services.steward_runtime_decision_agent import STEWARD_RUNTIME_DECISION_FUNCTION_NAME +from app.services.steward_slot_decision_agent import STEWARD_SLOT_DECISION_FUNCTION_NAME + + +class _FakeRuntime: + def __init__( + self, + payloads: dict[str, dict[str, Any] | None] | None = None, + *, + fail_functions: set[str] | None = None, + ) -> None: + self.payloads = payloads or {} + self.fail_functions = fail_functions or set() + self.called_functions: list[str] = [] + self.last_messages: list[dict[str, Any]] = [] + + def complete_with_tool_call(self, messages, tools, tool_choice, **kwargs): + function_name = str(tool_choice["function"]["name"]) + self.called_functions.append(function_name) + self.last_messages = messages + if function_name in self.fail_functions: + raise RuntimeError(f"{function_name} failed") + payload = self.payloads.get(function_name) + if payload is None: + return RuntimeToolCallResult(tool_call=None, calls=[]) + return RuntimeToolCallResult( + tool_call=RuntimeChatToolCall(name=function_name, arguments=payload), + calls=[ + RuntimeChatCallTrace( + slot=function_name, + provider="fake", + model="fake", + attempt=1, + status="succeeded", + ) + ], + ) + + +class _FailingGraphRuntime: + def __init__(self, runtime_chat_service) -> None: + self.runtime_chat_service = runtime_chat_service + + def decide_slot(self, request): + raise RuntimeError("langgraph runtime unavailable") + + def decide_runtime(self, request): + raise RuntimeError("langgraph runtime unavailable") + + +@pytest.fixture(autouse=True) +def _clear_settings_cache(): + get_settings.cache_clear() + yield + get_settings.cache_clear() + + +def test_graph_runtime_routes_slot_decision_through_langgraph_tool_node() -> None: + runtime = _FakeRuntime( + { + STEWARD_SLOT_DECISION_FUNCTION_NAME: { + "next_action": "ask_user", + "required_fields": ["expense_type", "time_range", "location", "reason", "transport_mode"], + "missing_fields": ["transport_mode"], + "question": "请问您这次打算怎么出行?", + "options": [ + {"field_key": "transport_mode", "label": "火车", "value": "火车"}, + {"field_key": "transport_mode", "label": "飞机", "value": "飞机"}, + ], + "rationale": "出行方式会影响交通费用测算。", + } + } + ) + + result = StewardGraphRuntime(runtime).decide_slot( + StewardSlotDecisionRequest( + task_type="expense_application", + user_message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署", + ontology_fields={ + "expense_type": "travel", + "time_range": "2026-02-20 至 2026-02-23", + "location": "上海", + "reason": "国网仿生产服务器部署", + }, + missing_fields=["transport_mode"], + ) + ) + + assert result.decision_source == "llm_function_call" + assert result.next_action == "ask_user" + assert result.missing_fields == ["transport_mode"] + assert runtime.called_functions == [STEWARD_SLOT_DECISION_FUNCTION_NAME] + + +def test_graph_runtime_slot_graph_falls_back_when_tool_node_fails() -> None: + runtime = _FakeRuntime(fail_functions={STEWARD_SLOT_DECISION_FUNCTION_NAME}) + + result = StewardGraphRuntime(runtime).decide_slot( + StewardSlotDecisionRequest( + task_type="expense_application", + user_message="上海出差,辅助国网仿生产部署", + ontology_fields={ + "expense_type": "travel", + "location": "上海", + "reason": "辅助国网仿生产部署", + }, + missing_fields=["transport_mode"], + ) + ) + + assert result.decision_source == "rule_fallback" + assert result.next_action == "ask_user" + assert result.missing_fields == ["transport_mode"] + assert any( + trace.get("slot") == "langgraph_slot_decision" + and trace.get("status") == "failed" + for trace in result.model_call_traces + ) + + +def test_graph_runtime_merges_memory_before_runtime_action_node() -> None: + runtime = _FakeRuntime({STEWARD_RUNTIME_DECISION_FUNCTION_NAME: None}) + + result = StewardGraphRuntime(runtime).decide_runtime( + StewardRuntimeDecisionRequest( + user_message="我坐高铁", + runtime_state={}, + context_json={ + "conversation_state": { + "steward_state": { + "active_flow": "travel_application", + "flows": { + "travel_application": { + "flow_id": "travel_application", + "intent": "travel_application_create", + "fields": { + "expense_type": "travel", + "time_range": "2026-07-02", + "location": "北京", + "reason": "客户现场支撑", + }, + "missing_fields": ["transport_mode"], + } + }, + } + } + }, + ) + ) + + assert result.decision_source == "rule_fallback" + assert result.next_action == "fill_current_slot" + assert result.field_key == "transport_mode" + assert result.field_value == "我坐高铁" + assert result.steward_state["flows"]["travel_application"]["fields"]["transport_mode"] == "我坐高铁" + assert result.steward_state["flows"]["travel_application"]["missing_fields"] == [] + assert runtime.called_functions == [STEWARD_RUNTIME_DECISION_FUNCTION_NAME] + assert "steward_state" in runtime.last_messages[-1]["content"] + + +def test_graph_runtime_selected_flow_action_node_skips_model_call() -> None: + runtime = _FakeRuntime() + + result = StewardGraphRuntime(runtime).decide_runtime( + StewardRuntimeDecisionRequest( + user_message="补办出差申请", + runtime_state={ + "steward_state": { + "active_flow": "", + "pending_flow_confirmation": { + "status": "pending", + "candidate_flows": [ + {"flow_id": "travel_application", "label": "补办出差申请"}, + {"flow_id": "travel_reimbursement", "label": "发起费用报销"}, + ], + }, + "flows": { + "travel_application": { + "flow_id": "travel_application", + "intent": "travel_application_create", + "status": "pending_flow_confirmation", + "fields": { + "time_range": "2026-02-20", + "location": "上海", + "expense_type": "travel", + "reason": "辅助国网仿生产环境部署", + }, + "missing_fields": ["transport_mode"], + } + }, + } + }, + ) + ) + + assert result.decision_source == "rule_fallback" + assert result.next_action == "continue_selected_flow" + assert result.target_task_id == "travel_application" + assert result.steward_state["active_flow"] == "travel_application" + assert runtime.called_functions == [] + + +def test_slot_endpoint_helper_falls_back_to_legacy_agent_when_langgraph_runtime_fails(monkeypatch) -> None: + monkeypatch.setenv("STEWARD_AGENT_RUNTIME", "langgraph") + get_settings.cache_clear() + monkeypatch.setattr(steward_endpoint, "StewardGraphRuntime", _FailingGraphRuntime) + runtime = _FakeRuntime({STEWARD_SLOT_DECISION_FUNCTION_NAME: None}) + + result = steward_endpoint._decide_steward_slot( + StewardSlotDecisionRequest( + task_type="expense_application", + user_message="上海出差,辅助国网仿生产部署", + ontology_fields={ + "expense_type": "travel", + "location": "上海", + "reason": "辅助国网仿生产部署", + }, + missing_fields=["transport_mode"], + ), + runtime, + ) + + assert result.decision_source == "rule_fallback" + assert result.next_action == "ask_user" + assert runtime.called_functions == [STEWARD_SLOT_DECISION_FUNCTION_NAME] + + +def test_runtime_endpoint_helper_falls_back_to_legacy_agent_when_langgraph_runtime_fails(monkeypatch) -> None: + monkeypatch.setenv("STEWARD_AGENT_RUNTIME", "langgraph") + get_settings.cache_clear() + monkeypatch.setattr(steward_endpoint, "StewardGraphRuntime", _FailingGraphRuntime) + runtime = _FakeRuntime({STEWARD_RUNTIME_DECISION_FUNCTION_NAME: None}) + + result = steward_endpoint._decide_steward_runtime( + StewardRuntimeDecisionRequest( + user_message="确认", + runtime_state={ + "pending_steward_action": { + "message_id": "msg-next-task", + "target_task_id": "task-reimbursement-meal", + } + }, + ), + runtime, + ) + + assert result.decision_source == "rule_fallback" + assert result.next_action == "continue_next_task" + assert result.target_message_id == "msg-next-task" + assert runtime.called_functions == [STEWARD_RUNTIME_DECISION_FUNCTION_NAME] diff --git a/server/tests/test_steward_intent_agent.py b/server/tests/test_steward_intent_agent.py index 0dcad50..f86b92f 100644 --- a/server/tests/test_steward_intent_agent.py +++ b/server/tests/test_steward_intent_agent.py @@ -2,6 +2,24 @@ from app.services.steward_intent_agent import ( STEWARD_INTENT_FUNCTION_NAME, StewardIntentAgent, ) +from app.schemas.steward import StewardPlanRequest + + +class _NoToolCallRuntimeChatService: + def __init__(self) -> None: + self.kwargs = {} + + def complete_with_tool_call(self, messages, **kwargs): + self.kwargs = kwargs + + class _Result: + tool_call = None + + @staticmethod + def calls_as_dicts(): + return [] + + return _Result() def test_steward_intent_tool_schema_supports_pending_flow_confirmation() -> None: @@ -12,9 +30,16 @@ def test_steward_intent_tool_schema_supports_pending_flow_confirmation() -> None function_schema = schema["function"] assert function_schema["name"] == STEWARD_INTENT_FUNCTION_NAME properties = function_schema["parameters"]["properties"] + task_schema = properties["tasks"]["items"] pending_schema = properties["pending_flow_confirmation"] candidate_schema = pending_schema["properties"]["candidate_flows"]["items"] + assert task_schema["properties"]["requested_action"]["enum"] == [ + "preview", + "save_draft", + "submit", + ] + assert "requested_action" in task_schema["required"] assert "pending_flow_confirmation" in properties assert pending_schema["properties"]["status"]["enum"] == ["none", "pending"] assert candidate_schema["properties"]["flow_id"]["enum"] == [ @@ -28,3 +53,18 @@ def test_steward_intent_tool_schema_supports_pending_flow_confirmation() -> None "reason", "transport_mode", ] + + +def test_steward_intent_agent_uses_ten_second_timeout_and_three_attempts() -> None: + runtime_chat = _NoToolCallRuntimeChatService() + agent = StewardIntentAgent(runtime_chat) + + agent.detect( + StewardPlanRequest(message="2026-02-20 至 2026-02-23,上海出差,火车,保存草稿。"), + base_date=__import__("datetime").date(2026, 6, 24), + canonical_fields=["expense_type", "time_range", "location", "reason", "transport_mode"], + ) + + assert runtime_chat.kwargs["timeout_seconds"] == 10 + assert runtime_chat.kwargs["max_attempts"] == 3 + assert runtime_chat.kwargs["use_failure_cooldown"] is False diff --git a/server/tests/test_steward_planner.py b/server/tests/test_steward_planner.py index d9938bb..b1e25a8 100644 --- a/server/tests/test_steward_planner.py +++ b/server/tests/test_steward_planner.py @@ -135,6 +135,7 @@ class ApplicationFunctionCallingIntentAgent: "task_type": "expense_application", "title": "北京出差申请", "summary": "明天前往北京出差3天,支撑国网仿生产部署。", + "requested_action": "save_draft", "confidence": 0.94, "ontology_fields": { "time_range": "明天", @@ -151,6 +152,52 @@ class ApplicationFunctionCallingIntentAgent: ) +class SingleTravelApplicationFunctionCallingIntentAgent: + def __init__(self) -> None: + self.calls = 0 + + def detect(self, request, *, base_date, canonical_fields): + self.calls += 1 + return StewardIntentAgentResult( + payload={ + "thinking_events": [ + { + "stage": "task_split", + "title": "识别出差申请草稿", + "content": "模型识别到用户要创建上海出差申请,并保存草稿。", + } + ], + "tasks": [ + { + "task_type": "expense_application", + "title": "上海出差申请", + "summary": "2026-02-20 至 2026-02-23 前往上海,国网仿生产服务器部署,火车出行。", + "requested_action": "save_draft", + "confidence": 0.95, + "ontology_fields": { + "time_range": "2026-02-20 至 2026-02-23", + "location": "上海", + "expense_type": "差旅", + "reason": "国网仿生产服务器部署", + "transport_mode": "火车", + }, + "missing_fields": [], + } + ], + "attachment_groups": [], + }, + model_call_traces=[ + { + "slot": "main", + "provider": "OpenAI Compatible", + "model": "gpt-test", + "attempt": 1, + "status": "succeeded", + } + ], + ) + + class PendingFlowFunctionCallingIntentAgent: def detect(self, request, *, base_date, canonical_fields): return StewardIntentAgentResult( @@ -255,6 +302,17 @@ def _create_steward_test_client_with_db(): return TestClient(app), TestingSessionLocal, app +def _build_fast_rule_fallback_steward_planner(_db): + return StewardPlannerService(intent_agent=EmptyFunctionCallingIntentAgent()) + + +def _patch_steward_endpoint_planner(monkeypatch) -> None: + monkeypatch.setattr( + "app.api.v1.endpoints.steward._build_steward_planner", + _build_fast_rule_fallback_steward_planner, + ) + + def _build_endpoint_application_claim( *, claim_no: str = "AP-202602-001", @@ -341,6 +399,7 @@ def test_steward_planner_enforces_application_transport_gap_after_function_calli result = StewardPlannerService(intent_agent=ApplicationFunctionCallingIntentAgent()).build_plan(payload) assert result.planning_source == "llm_function_call" + assert result.tasks[0].requested_action == "save_draft" assert result.tasks[0].missing_fields == ["transport_mode"] gap_events = [event for event in result.thinking_events if event.stage == "business_gap_check"] assert gap_events @@ -356,7 +415,7 @@ def test_steward_planner_returns_pending_flow_confirmation_from_llm() -> None: result = StewardPlannerService(intent_agent=PendingFlowFunctionCallingIntentAgent()).build_plan(payload) - assert result.planning_source == "rule_fallback" + assert result.planning_source == "llm_function_call" assert result.next_action == "confirm_flow" assert result.plan_status == "needs_flow_confirmation" assert result.pending_flow_confirmation.status == "pending" @@ -364,12 +423,12 @@ def test_steward_planner_returns_pending_flow_confirmation_from_llm() -> None: "travel_application", "travel_reimbursement", ] - assert result.candidate_flows[0].ontology_fields["time_range"] == "2026-02-20" + assert result.candidate_flows[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23" assert result.candidate_flows[0].ontology_fields["location"] == "上海" assert "申请" in result.summary and "报销" in result.summary -def test_steward_planner_skips_llm_for_single_ambiguous_travel_flow() -> None: +def test_steward_planner_tries_llm_before_rule_fallback_for_single_ambiguous_travel_flow() -> None: payload = StewardPlanRequest( message="\u0032\u6708\u0032\u0030-\u0032\u0033\u65e5\u53bb\u4e0a\u6d77\u51fa\u5dee\u8f85\u52a9\u56fd\u7f51\u4eff\u751f\u4ea7\u73af\u5883\u90e8\u7f72", client_now_iso="2026-06-15T09:30:00+08:00", @@ -379,7 +438,7 @@ def test_steward_planner_skips_llm_for_single_ambiguous_travel_flow() -> None: result = StewardPlannerService(intent_agent=intent_agent).build_plan(payload) - assert intent_agent.calls == 0 + assert intent_agent.calls == 1 assert result.planning_source == "rule_fallback" assert result.next_action == "confirm_flow" assert result.plan_status == "needs_flow_confirmation" @@ -404,6 +463,37 @@ def test_steward_planner_uses_llm_for_multi_financial_demands() -> None: assert result.model_call_traces[0]["status"] == "succeeded" +def test_steward_planner_uses_llm_for_single_explicit_travel_save_draft() -> None: + payload = StewardPlanRequest( + message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署,火车,保存草稿。", + client_now_iso="2026-06-24T14:20:00+08:00", + ) + intent_agent = SingleTravelApplicationFunctionCallingIntentAgent() + + result = StewardPlannerService(intent_agent=intent_agent).build_plan(payload) + + assert intent_agent.calls == 1 + assert result.planning_source == "llm_function_call" + assert result.tasks[0].requested_action == "save_draft" + assert result.tasks[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23" + assert result.tasks[0].ontology_fields["reason"] == "国网仿生产服务器部署" + assert result.model_call_traces[0]["status"] == "succeeded" + + +def test_steward_planner_rule_fallback_keeps_save_draft_action_and_date_range() -> None: + payload = StewardPlanRequest( + message="2026-02-20 至 2026-02-23,上海出差,国网仿生产服务器部署,火车,保存草稿。", + client_now_iso="2026-06-24T14:20:00+08:00", + ) + + result = StewardPlannerService(intent_agent=EmptyFunctionCallingIntentAgent()).build_plan(payload) + + assert result.planning_source == "rule_fallback" + assert result.tasks[0].requested_action == "save_draft" + assert result.tasks[0].ontology_fields["time_range"] == "2026-02-20 至 2026-02-23" + assert result.tasks[0].ontology_fields["reason"] == "国网仿生产服务器部署" + + def test_steward_planner_overrides_llm_direct_application_for_ambiguous_travel_flow() -> None: payload = StewardPlanRequest( message="2月20-23日去上海出差辅助国网仿生产环境部署", @@ -412,7 +502,7 @@ def test_steward_planner_overrides_llm_direct_application_for_ambiguous_travel_f result = StewardPlannerService(intent_agent=AmbiguousApplicationFunctionCallingIntentAgent()).build_plan(payload) - assert result.planning_source == "rule_fallback" + assert result.planning_source == "llm_function_call" assert result.next_action == "confirm_flow" assert result.plan_status == "needs_flow_confirmation" assert result.tasks == [] @@ -557,6 +647,34 @@ def test_steward_planner_keeps_bare_reimbursement_intent_generic() -> None: assert task.ontology_fields.get("expense_type") == "other" assert "reason" not in task.ontology_fields assert task.missing_fields == ["time_range", "reason"] + assert [step.action_type for step in task.action_steps] == [ + "fill_reimbursement_fields", + "build_reimbursement_preview", + "validate_required_fields", + "create_reimbursement_draft", + ] + assert task.action_steps[-1].status == "blocked" + + +def test_steward_planner_builds_reimbursement_action_steps() -> None: + payload = StewardPlanRequest( + message="我要报销昨天客户现场沟通的交通费", + user_id="u001", + client_now_iso="2026-06-04T09:30:00+08:00", + context_json={"review_form_values": {"amount": "128.50"}}, + ) + + result = StewardPlannerService().build_plan(payload) + + assert result.tasks[0].task_type == "reimbursement" + assert [step.action_type for step in result.tasks[0].action_steps] == [ + "fill_reimbursement_fields", + "build_reimbursement_preview", + "validate_required_fields", + "create_reimbursement_draft", + ] + assert result.tasks[0].action_steps[0].payload["ontology_fields"]["amount"] == "128.50" + assert result.tasks[0].action_steps[-1].status == "planned" def test_steward_planner_treats_future_travel_without_apply_word_as_application() -> None: @@ -636,7 +754,8 @@ def test_steward_planner_builds_travel_attachment_group_with_exclusions() -> Non assert len(attachment_actions) == 1 -def test_steward_stream_endpoint_emits_thinking_before_plan() -> None: +def test_steward_stream_endpoint_emits_thinking_before_plan(monkeypatch) -> None: + _patch_steward_endpoint_planner(monkeypatch) client = TestClient(create_app()) with client.stream( @@ -660,7 +779,8 @@ def test_steward_stream_endpoint_emits_thinking_before_plan() -> None: assert events[-1]["data"]["tasks"][0]["ontology_fields"]["time_range"] == "2026-06-03" -def test_steward_plan_endpoint_persists_application_and_reimbursement_state() -> None: +def test_steward_plan_endpoint_persists_application_and_reimbursement_state(monkeypatch) -> None: + _patch_steward_endpoint_planner(monkeypatch) client = TestClient(create_app()) response = client.post( @@ -685,7 +805,8 @@ def test_steward_plan_endpoint_persists_application_and_reimbursement_state() -> assert all("invented_field" not in flow["fields"] for flow in state["flows"].values()) -def test_steward_plan_endpoint_queries_applications_before_ambiguous_travel_choice() -> None: +def test_steward_plan_endpoint_queries_applications_before_ambiguous_travel_choice(monkeypatch) -> None: + _patch_steward_endpoint_planner(monkeypatch) client, SessionLocal, app = _create_steward_test_client_with_db() try: response = client.post( diff --git a/server/uv.lock b/server/uv.lock index 3180c32..8d1f773 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -933,6 +933,12 @@ 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 = "jieba" +version = "0.42.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b726522d7ebed54f2278ce28ba9438e3135ab0278d9792a2/jieba-0.42.1.tar.gz", hash = "sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2", size = 19214172, upload-time = "2020-01-20T14:27:23.5Z" } + [[package]] name = "json-repair" version = "0.59.10" @@ -942,6 +948,143 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/87/49b20c6b81493d55c311f711ed87319d0fbad8bd0bbfbe36e52103af36bd/json_repair-0.59.10-py3-none-any.whl", hash = "sha256:5468fa3eaadcc9b4a5646776bc4176e2fe5f374b5848a15f468cce3b60e3db0e", size = 47742, upload-time = "2026-05-14T06:41:49.812Z" }, ] +[[package]] +name = "jsonpatch" +version = "1.33" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpointer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/78/18813351fe5d63acad16aec57f94ec2b70a09e53ca98145589e185423873/jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c", size = 21699, upload-time = "2023-06-26T12:07:29.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/07/02e16ed01e04a374e644b575638ec7987ae846d25ad97bcc9945a3ee4b0e/jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade", size = 12898, upload-time = "2023-06-16T21:01:28.466Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, +] + +[[package]] +name = "langchain-core" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/e3/bea6d0080acf183332f24dcd74c208aee5857cf8f783c3fb0bd86027d8fb/langchain_core-1.4.8.tar.gz", hash = "sha256:5bf1f8411077c904182ad8f975943d36adcbf579c4e017b3a118b719229ebf9a", size = 957974, upload-time = "2026-06-18T19:39:23.636Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/d6/bdf6f0481cc57ef300d6b1eb48cf1400c0409be715d6eb3cabadd1142a09/langchain_core-1.4.8-py3-none-any.whl", hash = "sha256:d84c28b05e3ba8d4271d0827aad5b592ccdaaf986e76768c23503f0a2045e8aa", size = 557416, upload-time = "2026-06-18T19:39:21.902Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.18" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/59/b5959aea96faa9146e2e49a7a22882b3528c62efafe9a6a95beab30c2305/langchain_protocol-0.0.18.tar.gz", hash = "sha256:ec3e11782f1ed0c9db38e5a9ed01b0e7a0d3fba406faa8aef6594b73c56a63e6", size = 6150, upload-time = "2026-06-18T17:08:26.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/2e/d82db9eec13ad0f72e7aaad5c4bc730ab111934fdc83c85523206eb9b0a0/langchain_protocol-0.0.18-py3-none-any.whl", hash = "sha256:70b53a86fbf9cedc863555effe44da192ab02d556ddbf2cf95b8873adcf41b5a", size = 7221, upload-time = "2026-06-18T17:08:25.996Z" }, +] + +[[package]] +name = "langgraph" +version = "1.2.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/7a/ea09b05bb0cbddfa43bd34fc581357e87fc3f21a751cc0d419688c3106da/langgraph-1.2.6.tar.gz", hash = "sha256:f9b45a34f13930c94d96cdb76277447ad2cc70ec2d18cd2764d7fdadb36cdc1b", size = 714400, upload-time = "2026-06-18T20:58:21.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/32/772db1b00a9fe42f50320d1aa20caefb76e621eff1f7218b9918093d631d/langgraph-1.2.6-py3-none-any.whl", hash = "sha256:1cf94d3ca124f84f77ce408fa1b06c3dee680a8aafffe364a8fd5d7d03eb8695", size = 246132, upload-time = "2026-06-18T20:58:20.335Z" }, +] + +[[package]] +name = "langgraph-checkpoint" +version = "4.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "ormsgpack" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/b4/71425e3e38be92611300b9cc5e46a5bf98ab23f5ea8a75b73d02a2f1413c/langgraph_checkpoint-4.1.1-py3-none-any.whl", hash = "sha256:25d29144b082827218e7bc3f1e9b0566a4bb007895cd6cc26f66a8428739f56e", size = 56212, upload-time = "2026-05-22T16:57:37.203Z" }, +] + +[[package]] +name = "langgraph-prebuilt" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "langchain-core" }, + { name = "langgraph-checkpoint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, +] + +[[package]] +name = "langgraph-sdk" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, + { name = "orjson" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" }, +] + +[[package]] +name = "langsmith" +version = "0.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, + { name = "websockets" }, + { name = "xxhash" }, + { name = "zstandard" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/26/b72987d947278f63ec1e85f01ce85ca7ab2621c7efc0845d4a3a8e5d5dfb/langsmith-0.9.1.tar.gz", hash = "sha256:e5eb905224d156bcece4985285c55b51fffcb06c9353b2c4adb42e1c48b0d05d", size = 4557557, upload-time = "2026-06-23T17:04:23.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/42/13c67eb24cddb368df2c8af13e319c86b1d270c90f5b8c9e8baed3593e04/langsmith-0.9.1-py3-none-any.whl", hash = "sha256:1160bf667af63d9bc081821f1df351cb84f7875740858f2a97ffef62b21800a9", size = 578856, upload-time = "2026-06-23T17:04:21.47Z" }, +] + [[package]] name = "lightrag-hku" version = "1.4.16" @@ -1286,6 +1429,122 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, ] +[[package]] +name = "orjson" +version = "3.11.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/0c/964746fcafbd16f8ff53219ad9f6b412b34f345c75f384ad434ceaadb538/orjson-3.11.9.tar.gz", hash = "sha256:4fef17e1f8722c11587a6ef18e35902450221da0028e65dbaaa543619e68e48f", size = 5599163, upload-time = "2026-05-06T15:11:08.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/51/3fb9e65ae76ee97bd611869a503fa3fc0a6e81dd8b737cf3003f682df7ff/orjson-3.11.9-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f01c4818b3fc9b0da8e096722a84318071eaa118df35f6ed2344da0e73a5444f", size = 228522, upload-time = "2026-05-06T15:09:35.362Z" }, + { url = "https://files.pythonhosted.org/packages/16/fa/9d54b07cb3f3b0bfd57841478e42d7a0ece4a9f49f9907eecf5a45461687/orjson-3.11.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:3ebca4179031ee716ed076ffadc29428e900512f6fccee8614c9983157fcf19c", size = 128463, upload-time = "2026-05-06T15:09:37.063Z" }, + { url = "https://files.pythonhosted.org/packages/88/b1/6ceafc2eefd0a553e3be77ce6c49d107e772485d9568629376171c50e634/orjson-3.11.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48ee05097750de0ff69ed5b7bbcf0732182fd57a24043dcc2a1da780a5ead3a5", size = 132306, upload-time = "2026-05-06T15:09:38.299Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/f11311285324a40aab1e3031385c50b635a7cd0734fdaf60c7e89a696f60/orjson-3.11.9-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6082706765a95a6680d812e1daf1c0cfe8adec7831b3ff3b625693f3b461b1c", size = 127988, upload-time = "2026-05-06T15:09:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/9e/85/0ef63bcf1337f44031ce9b91b1919563f62a37527b3ea4368bb15a22e5d7/orjson-3.11.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:277fefe9d76ee17eb14debf399e3533d4d63b5f677a4d3719eb763536af1f4bd", size = 135188, upload-time = "2026-05-06T15:09:40.957Z" }, + { url = "https://files.pythonhosted.org/packages/05/94/b0d27090ea8a2095db3c2bd1b1c96f96f19bbb494d7fef33130e846e613d/orjson-3.11.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03db380e3780fa0015ed776a90f20e8e20bb11dde13b216ce19e5718e3dfba62", size = 145937, upload-time = "2026-05-06T15:09:42.249Z" }, + { url = "https://files.pythonhosted.org/packages/09/eb/75d50c29c05b8054013e221e598820a365c8e64065312e75e202ed880709/orjson-3.11.9-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33d7d766701847dc6729846362dc27895d2f2d2251264f9d10e7cb9878194877", size = 132758, upload-time = "2026-05-06T15:09:43.945Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/360686f39348aa88827cb6fbf7dc606fd41c831a35235e1abf1db8e3a9e6/orjson-3.11.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147302878da387104b66bb4a8b0227d1d487e976ce41a8501916161072ed87b1", size = 133971, upload-time = "2026-05-06T15:09:45.239Z" }, + { url = "https://files.pythonhosted.org/packages/0e/30/3178eb16f3221aeef068b6f1f1ebe05f656ea5c6dffe9f6c917329fe17a3/orjson-3.11.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3513550321f8c8c811a7c3297b8a630e82dc08e4c10216d07703c997776236cd", size = 141685, upload-time = "2026-05-06T15:09:46.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f1/ff2f19ed0225f9680fafa42febca3570dd59444ebf190980738d376214c2/orjson-3.11.9-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c5d001196b89fa9cf0a4ab79766cd835b991a166e4b621ba95089edc50c429ff", size = 415167, upload-time = "2026-05-06T15:09:48.312Z" }, + { url = "https://files.pythonhosted.org/packages/9b/61/863bddf0da6e9e586765414debd54b4e58db05f560902b6d00658cb88636/orjson-3.11.9-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16969c9d369c98eb084889c6e4d2d39b77c7eb38ceccf8da2a9fff62ae908980", size = 147913, upload-time = "2026-05-06T15:09:49.733Z" }, + { url = "https://files.pythonhosted.org/packages/b6/8a/4081492586d75b073d60c5271a8d0f05a0955cabf1e34c8473f6fcd84235/orjson-3.11.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:63e0efbc991250c0b3143488fa57d95affcabbfc63c99c48d625dd37779aafe2", size = 136959, upload-time = "2026-05-06T15:09:51.311Z" }, + { url = "https://files.pythonhosted.org/packages/0d/bd/70b6ab193594d7abb875320c0a7c8335e846f28968c432c31042409c3c8d/orjson-3.11.9-cp311-cp311-win32.whl", hash = "sha256:14ed654580c1ed2bc217352ec82f91b047aef82951aa71c7f64e0dcb03c0e180", size = 131533, upload-time = "2026-05-06T15:09:52.637Z" }, + { url = "https://files.pythonhosted.org/packages/3f/17/1a1a228183d62d1b77e2c30d210f47dd4768b310ebe1607c63e3c0e3a71e/orjson-3.11.9-cp311-cp311-win_amd64.whl", hash = "sha256:57ea77fb70a448ce87d18fca050193202a3da5e54598f6501ca5476fb66cfe02", size = 127106, upload-time = "2026-05-06T15:09:54.204Z" }, + { url = "https://files.pythonhosted.org/packages/b8/95/285de5fa296d09681ee9c546cd4a8aeb773b701cf343dc125994f4d52953/orjson-3.11.9-cp311-cp311-win_arm64.whl", hash = "sha256:19b72ed11572a2ee51a67a903afbe5af504f84ed6f529c0fe44b0ab3fb5cc697", size = 126848, upload-time = "2026-05-06T15:09:55.551Z" }, + { url = "https://files.pythonhosted.org/packages/16/6d/11867a3ffa3a3608d84a4de51ef4dd0896d6b5cc9132fbe1daf593e677bc/orjson-3.11.9-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9ef6fe90aadef185c7b128859f40beb24720b4ecea95379fc9000931179c3a49", size = 228515, upload-time = "2026-05-06T15:09:57.265Z" }, + { url = "https://files.pythonhosted.org/packages/24/75/05912954c8b288f34fcf5cd4b9b071cb4f6e77b9961e175e56ebb258089f/orjson-3.11.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e5c9b8f28e726e97d97696c826bc7bea5d71cecd63576dba92924a32c1961291", size = 128409, upload-time = "2026-05-06T15:09:59.063Z" }, + { url = "https://files.pythonhosted.org/packages/ab/86/1c3a47df3bc8191ea9ac51603bbb872a95167a364320c269f2557911f406/orjson-3.11.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a473dbb4162108b27901492546f83c76fdcea3d0eadff00ae7a07e18dcce09", size = 132106, upload-time = "2026-05-06T15:10:00.798Z" }, + { url = "https://files.pythonhosted.org/packages/d7/cf/b33b5f3e695ae7d63feef9d915c37cc3b8f465493dcd4f8e0b4c697a2366/orjson-3.11.9-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:011382e2a60fda9d46f1cdee31068cfc52ffe952b587d683ec0463002802a0f4", size = 127864, upload-time = "2026-05-06T15:10:02.15Z" }, + { url = "https://files.pythonhosted.org/packages/31/6a/6cf69385a58208024fcb8c014e2141b8ce838aba6492b589f8acfff97fab/orjson-3.11.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2d3dc759490128c5c1711a53eeaa8ee1d437fd0038ffd2b6008abf46db3f882", size = 135213, upload-time = "2026-05-06T15:10:03.515Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f8/0b1bd3e8f2efcdd376af5c8cfd79eaf13f018080c0089c80ebd724e3c7fb/orjson-3.11.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8ea516b3726d190e1b4297e6f4e7a8650347ae053868a18163b4dd3641d1fff", size = 145994, upload-time = "2026-05-06T15:10:05.083Z" }, + { url = "https://files.pythonhosted.org/packages/f3/59/dab79f61044c529d2c81aecdc589b1f833a1c8dec11ba3b1c2498a02ca7e/orjson-3.11.9-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:380cdce7ba24989af81d0a7013d0aaec5d0e2a21734c0e2681b1bc4f141957fe", size = 132744, upload-time = "2026-05-06T15:10:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a4/82b7a2fe5d8a67a59ed831b24d59a3d46ea7d207b66e1602d376541d94a6/orjson-3.11.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be4fa4f0af7fa18951f7ab3fc2148e223af211bf03f59e1c6034ec3f97f21d61", size = 134014, upload-time = "2026-05-06T15:10:08.213Z" }, + { url = "https://files.pythonhosted.org/packages/50/c7/375e83a76851b73b2e39f3bcf0e5a19e2b89bad13e5bca97d0b293d27f24/orjson-3.11.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a8f5f8bc7ce7d59f08d9f99fa510c06496164a24cb5f3d34537dbd9ca30132e2", size = 141509, upload-time = "2026-05-06T15:10:09.595Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/49d5d82a3d3097f641f094f552131f1e2723b0b8cb0fa2874ab65ecfffa6/orjson-3.11.9-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:4d7fde5501b944f83b3e665e1b31343ff6e154b15560a16b7130ea1e594a4206", size = 415127, upload-time = "2026-05-06T15:10:11.049Z" }, + { url = "https://files.pythonhosted.org/packages/3a/dc/7446c538590d55f455647e5f3c61fc33f7108714e7afcffa6a2a033f8350/orjson-3.11.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cde1a448023ba7d5bb4c01c5afb48894380b5e4956e0627266526587ef4e535f", size = 148025, upload-time = "2026-05-06T15:10:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/df/e5/4d2d8af06f788329b4f78f8cc3679bb395392fcaa1e4d8d3c33e85308fa4/orjson-3.11.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:71e63adb0e1f1ed5d9e168f50a91ceb93ae6420731d222dc7da5c69409aa47aa", size = 136943, upload-time = "2026-05-06T15:10:14.405Z" }, + { url = "https://files.pythonhosted.org/packages/06/69/850264ccf6d80f6b174620d30a87f65c9b1490aba33fe6b62798e618cad3/orjson-3.11.9-cp312-cp312-win32.whl", hash = "sha256:2d057a602cdd19a0ad680417527c45b6961a095081c0f46fe0e03e304aac6470", size = 131606, upload-time = "2026-05-06T15:10:15.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/973a43fc9c55e20f2051e9830997649f669be0cb3ca52192087c0143f118/orjson-3.11.9-cp312-cp312-win_amd64.whl", hash = "sha256:59e403b1cc5a676da8eaf31f6254801b7341b3e29efa85f92b48d272637e77be", size = 127101, upload-time = "2026-05-06T15:10:17.129Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ae/495470f0e4a18f73fa10b7f6b84b464ec4cc5291c4e0c7c2a6c400bef006/orjson-3.11.9-cp312-cp312-win_arm64.whl", hash = "sha256:9af678d6488357948f1f84c6cd1c1d397c014e1ae2f98ae082a44eb48f602624", size = 126736, upload-time = "2026-05-06T15:10:18.645Z" }, + { url = "https://files.pythonhosted.org/packages/32/33/93fcc25907235c344ae73122f8a4e01d2d393ef062b4af7d2e2487a32c37/orjson-3.11.9-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4bab1b2d6141fe7b32ae71dac905666ece4f94936efbfb13d55bb7739a3a6021", size = 228458, upload-time = "2026-05-06T15:10:20.079Z" }, + { url = "https://files.pythonhosted.org/packages/8f/27/b1e6dadb3c080313c03fdd8067b85e6a0460c7d8d6a1c3984ef77b904e4d/orjson-3.11.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:844417969855fc7a41be124aafe83dc424592a7f77cd4501900c67307122b92c", size = 128368, upload-time = "2026-05-06T15:10:21.549Z" }, + { url = "https://files.pythonhosted.org/packages/21/0f/c9ede0bf052f6b4051e64a7d4fa91b725cccf8321a6a786e86eb03519f00/orjson-3.11.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffe02797b5e9f3a9d8292ddcd289b474ad13e81ad83cd1891a240811f1d2cb81", size = 132070, upload-time = "2026-05-06T15:10:23.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/26/d398e28048dc18205bbe812f2c88cb9b40313db2470778e25964796458fe/orjson-3.11.9-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e4eed3b200023042814d2fc8a5d2e880f13b52e1ed2485e83da4f3962f7dc1a", size = 127892, upload-time = "2026-05-06T15:10:24.714Z" }, + { url = "https://files.pythonhosted.org/packages/66/60/52b0054c4c700d5aa7fc5b7ca96917400d8f061307778578e67a10e25852/orjson-3.11.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8aff7da9952a5ad1cef8e68017724d96c7b9a66e99e91d6252e1b133d67a7b10", size = 135217, upload-time = "2026-05-06T15:10:26.084Z" }, + { url = "https://files.pythonhosted.org/packages/d5/97/1e3dc2b2a28b7b2528f403d2fc1d79ec5f39af3bc143ab65d3ec26426385/orjson-3.11.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d4e98d6f3b8afed8bc8cd9718ec0cdf46661826beefb53fe8eafb37f2bf0362", size = 145980, upload-time = "2026-05-06T15:10:28.062Z" }, + { url = "https://files.pythonhosted.org/packages/fc/39/31fbfe7850f2de32dee7e7e5c09f26d403ab01e440ac96001c6b01ad3c99/orjson-3.11.9-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a81d52442a7c99b3662333235b3adf96a1715864658b35bb797212be7bddb97", size = 132738, upload-time = "2026-05-06T15:10:29.727Z" }, + { url = "https://files.pythonhosted.org/packages/a1/08/dca0082dd2a194acb93e5457e73455388e2e2ca464a2672449a9ddbb679d/orjson-3.11.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e39364e726a8fff737309aff059ff67d8a8c8d5b677be7bb49a8b3e84b7e218", size = 134033, upload-time = "2026-05-06T15:10:31.152Z" }, + { url = "https://files.pythonhosted.org/packages/11/d4/5bdb0626801230139987385554c5d4c42255218ac906525bf4347f22cd95/orjson-3.11.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4fd66214623f1b17501df9f0543bef0b833979ab5b6ded1e1d123222866aa8c9", size = 141492, upload-time = "2026-05-06T15:10:32.641Z" }, + { url = "https://files.pythonhosted.org/packages/fa/88/a21fb53b3ede6703aede6dce4710ed4111e5b201cfa6bbff5e544f9d47d7/orjson-3.11.9-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8ecc30f10465fa1e0ce13fd01d9e22c316e5053a719a8d915d4545a09a5ff677", size = 415087, upload-time = "2026-05-06T15:10:34.438Z" }, + { url = "https://files.pythonhosted.org/packages/3d/57/1b30daf70f0d8180e9a73cefbfbdd99e4bf19eb020466502b01fba7e0e50/orjson-3.11.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:97db4c94a7db398a5bd636273324f0b3fd58b350bbbac8bb380ceb825a9b40f4", size = 148031, upload-time = "2026-05-06T15:10:36.358Z" }, + { url = "https://files.pythonhosted.org/packages/04/83/45fbb6d962e260807f99441db9613cee868ceda4baceda59b3720a563f97/orjson-3.11.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f78cf8fec5bd627f4082b8dfeac7871b43d7f3274904492a43dab39f18a19a0", size = 136915, upload-time = "2026-05-06T15:10:38.013Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cc/2d10025f9056d376e4127ec05a5808b218d46f035fdc08178a5411b34250/orjson-3.11.9-cp313-cp313-win32.whl", hash = "sha256:d4087e5c0209a0a8efe4de3303c234b9c44d1174161dcd851e8eea07c7560b32", size = 131613, upload-time = "2026-05-06T15:10:39.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/2775ff28bfe883b9aa1ff348300542eb2ef1ee18d8ae0e3a49846817a865/orjson-3.11.9-cp313-cp313-win_amd64.whl", hash = "sha256:051b102c93b4f634e89f3866b07b9a9a98915ada541f4ec30f177067b2694979", size = 127086, upload-time = "2026-05-06T15:10:41.262Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/d26799e580939e32a7da9a39531bc9e58e15ca32ffaa6a8cb3e9bb0d22cd/orjson-3.11.9-cp313-cp313-win_arm64.whl", hash = "sha256:cce9127885941bd28f080cecf1f1d288336b7e0d812c345b08be88b572796254", size = 126696, upload-time = "2026-05-06T15:10:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/8e/eb/5da01e356015aee6ecfa1187ced87aef51364e306f5e695dd52719bf0e78/orjson-3.11.9-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b6ef1979adc4bc243523f1a2ba91418030a8e29b0a99cbe7e0e2d6807d4dce6e", size = 228465, upload-time = "2026-05-06T15:10:44.097Z" }, + { url = "https://files.pythonhosted.org/packages/64/62/3e0e0c14c957133bcd855395c62b55ed4e3b0af23ffea11b032cb1dcbdb1/orjson-3.11.9-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:f36b7f32c7c0db4a719f1fc5824db4a9c6f8bd1a354debb91faf26ebf3a4c71e", size = 128364, upload-time = "2026-05-06T15:10:45.839Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/07d8aa117211a8ed7630bda80c8c0b14d04e0f8dcf99bcf49656e4a710eb/orjson-3.11.9-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08f4d8ebb44925c794e535b2bebc507cebf32209df81de22ae285fb0d8d66de0", size = 132063, upload-time = "2026-05-06T15:10:47.267Z" }, + { url = "https://files.pythonhosted.org/packages/d6/ec/4acaf21483e18aa945be74a474c74b434f284b549f275a0a39b9f98956e9/orjson-3.11.9-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6cc7923789694fd58f001cbcac7e47abc13af4d560ebbfcf3b41a8b1a0748124", size = 122356, upload-time = "2026-05-06T15:10:48.765Z" }, + { url = "https://files.pythonhosted.org/packages/13/d8/5f0555e7638801323b7a75850f92e7dfa891bc84fe27a1ba4449170d1200/orjson-3.11.9-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea5c46eb2d3af39e806b986f4b09d5c2706a1f5afde3cbf7544ce6616127173c", size = 129592, upload-time = "2026-05-06T15:10:50.13Z" }, + { url = "https://files.pythonhosted.org/packages/b6/30/ed9860412a3603ceb3c5955bfd72d28b9d0e7ba6ed81add14f83d7114236/orjson-3.11.9-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f5d89a2ed90731df3be64bab0aa44f78bff39fdc9d71c291f4a8023aa46425b7", size = 140491, upload-time = "2026-05-06T15:10:51.582Z" }, + { url = "https://files.pythonhosted.org/packages/d0/17/adc514dea7ac7c505527febf884934b815d34f0c7b8693c1a8b39c5c4a57/orjson-3.11.9-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25e4aed0312d292c09f61af25bba34e0b2c88546041472b09088c39a4d828af1", size = 127309, upload-time = "2026-05-06T15:10:53.329Z" }, + { url = "https://files.pythonhosted.org/packages/76/3e/c0b690253f0b82d86e99949af13533363acfb5432ecb5d53dd5b3bce9c34/orjson-3.11.9-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aaea64f3f467d22e70eeed68bdccb3bc4f83f650446c4a03c59f2cba28a108db", size = 134030, upload-time = "2026-05-06T15:10:54.988Z" }, + { url = "https://files.pythonhosted.org/packages/c1/7a/bc82a0bb25e9faaf92dc4d9ef002732efc09737706af83e346788641d4a7/orjson-3.11.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a028425d1b440c5d92a6be1e1a020739dfe67ea87d96c6dbe828c1b30041728b", size = 141482, upload-time = "2026-05-06T15:10:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/01/55/e69188b939f77d5d32a9833745ace31ea5ccae3ab613a1ec185d3cd2c4fb/orjson-3.11.9-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5b192c6cf397e4455b11523c5cf2b18ed084c1bbd61b6c0926344d2129481972", size = 415178, upload-time = "2026-05-06T15:10:58.446Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/b8a5a7ac527e80b9cb11d51e3f6689b709279183264b9ec5c7bc680bb8b5/orjson-3.11.9-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea407d4ccf5891d667d045fecae97a7a1e5e87b3b97f97ae1803c2e741130be0", size = 148089, upload-time = "2026-05-06T15:11:00.441Z" }, + { url = "https://files.pythonhosted.org/packages/97/4e/00503f64204bf859b37213a63927028f30fb6268cd8677fb0a5ad48155e1/orjson-3.11.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5f63aaf97afd9f6dec5b1a68e1b8da12bfccb4cb9a9a65c3e0b6c847849e7586", size = 136921, upload-time = "2026-05-06T15:11:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ba/a23b82a0a8d0ed7bed4e5f5035aae751cad4ff6a1e8d2ecd14d8860f5929/orjson-3.11.9-cp314-cp314-win32.whl", hash = "sha256:e30ab17845bb9fa54ccf67fa4f9f5282652d54faa6d17452f47d0f369d038673", size = 131638, upload-time = "2026-05-06T15:11:03.696Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/0c6798456bade745c75c452342dabacce5798196483e77e643be1f53877d/orjson-3.11.9-cp314-cp314-win_amd64.whl", hash = "sha256:32ef5f4283a3be81913947d19608eacb7c6608026851123790cd9cc8982af34b", size = 127078, upload-time = "2026-05-06T15:11:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/16/21/5a3f1e8913103b703a436a5664238e5b965ec392b555fe68943ea3691e6b/orjson-3.11.9-cp314-cp314-win_arm64.whl", hash = "sha256:eebdbdeef0094e4f5aefa20dcd4eb2368ab5e7a3b4edea27f1e7b2892e009cf9", size = 126687, upload-time = "2026-05-06T15:11:06.602Z" }, +] + +[[package]] +name = "ormsgpack" +version = "1.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/0c/f1761e21486942ab9bb6feaebc610fa074f7c5e496e6962dea5873348077/ormsgpack-1.12.2.tar.gz", hash = "sha256:944a2233640273bee67521795a73cf1e959538e0dfb7ac635505010455e53b33", size = 39031, upload-time = "2026-01-18T20:55:28.023Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/08/8b68f24b18e69d92238aa8f258218e6dfeacf4381d9d07ab8df303f524a9/ormsgpack-1.12.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bd5f4bf04c37888e864f08e740c5a573c4017f6fd6e99fa944c5c935fabf2dd9", size = 378266, upload-time = "2026-01-18T20:55:59.876Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/29fc13044ecb7c153523ae0a1972269fcd613650d1fa1a9cec1044c6b666/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34d5b28b3570e9fed9a5a76528fc7230c3c76333bc214798958e58e9b79cc18a", size = 203035, upload-time = "2026-01-18T20:55:30.59Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c2/00169fb25dd8f9213f5e8a549dfb73e4d592009ebc85fbbcd3e1dcac575b/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3708693412c28f3538fb5a65da93787b6bbab3484f6bc6e935bfb77a62400ae5", size = 210539, upload-time = "2026-01-18T20:55:48.569Z" }, + { url = "https://files.pythonhosted.org/packages/1b/33/543627f323ff3c73091f51d6a20db28a1a33531af30873ea90c5ac95a9b5/ormsgpack-1.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43013a3f3e2e902e1d05e72c0f1aeb5bedbb8e09240b51e26792a3c89267e181", size = 212401, upload-time = "2026-01-18T20:56:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/e8/5d/f70e2c3da414f46186659d24745483757bcc9adccb481a6eb93e2b729301/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7c8b1667a72cbba74f0ae7ecf3105a5e01304620ed14528b2cb4320679d2869b", size = 387082, upload-time = "2026-01-18T20:56:12.047Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/06e8dc920c7903e051f30934d874d4afccc9bb1c09dcaf0bc03a7de4b343/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:df6961442140193e517303d0b5d7bc2e20e69a879c2d774316125350c4a76b92", size = 482346, upload-time = "2026-01-18T20:56:05.152Z" }, + { url = "https://files.pythonhosted.org/packages/66/c4/f337ac0905eed9c393ef990c54565cd33644918e0a8031fe48c098c71dbf/ormsgpack-1.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c6a4c34ddef109647c769d69be65fa1de7a6022b02ad45546a69b3216573eb4a", size = 425181, upload-time = "2026-01-18T20:55:37.83Z" }, + { url = "https://files.pythonhosted.org/packages/78/29/6d5758fabef3babdf4bbbc453738cc7de9cd3334e4c38dd5737e27b85653/ormsgpack-1.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:73670ed0375ecc303858e3613f407628dd1fca18fe6ac57b7b7ce66cc7bb006c", size = 117182, upload-time = "2026-01-18T20:55:31.472Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/17a15549233c37e7fd054c48fe9207492e06b026dbd872b826a0b5f833b6/ormsgpack-1.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2be829954434e33601ae5da328cccce3266b098927ca7a30246a0baec2ce7bd", size = 111464, upload-time = "2026-01-18T20:55:38.811Z" }, + { url = "https://files.pythonhosted.org/packages/4c/36/16c4b1921c308a92cef3bf6663226ae283395aa0ff6e154f925c32e91ff5/ormsgpack-1.12.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7a29d09b64b9694b588ff2f80e9826bdceb3a2b91523c5beae1fab27d5c940e7", size = 378618, upload-time = "2026-01-18T20:55:50.835Z" }, + { url = "https://files.pythonhosted.org/packages/c0/68/468de634079615abf66ed13bb5c34ff71da237213f29294363beeeca5306/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b39e629fd2e1c5b2f46f99778450b59454d1f901bc507963168985e79f09c5d", size = 203186, upload-time = "2026-01-18T20:56:11.163Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/d756e01961442688b7939bacd87ce13bfad7d26ce24f910f6028178b2cc8/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:958dcb270d30a7cb633a45ee62b9444433fa571a752d2ca484efdac07480876e", size = 210738, upload-time = "2026-01-18T20:56:09.181Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ba/795b1036888542c9113269a3f5690ab53dd2258c6fb17676ac4bd44fcf94/ormsgpack-1.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58d379d72b6c5e964851c77cfedfb386e474adee4fd39791c2c5d9efb53505cc", size = 212569, upload-time = "2026-01-18T20:56:06.135Z" }, + { url = "https://files.pythonhosted.org/packages/6c/aa/bff73c57497b9e0cba8837c7e4bcab584b1a6dbc91a5dd5526784a5030c8/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8463a3fc5f09832e67bdb0e2fda6d518dc4281b133166146a67f54c08496442e", size = 387166, upload-time = "2026-01-18T20:55:36.738Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cf/f8283cba44bcb7b14f97b6274d449db276b3a86589bdb363169b51bc12de/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:eddffb77eff0bad4e67547d67a130604e7e2dfbb7b0cde0796045be4090f35c6", size = 482498, upload-time = "2026-01-18T20:55:29.626Z" }, + { url = "https://files.pythonhosted.org/packages/05/be/71e37b852d723dfcbe952ad04178c030df60d6b78eba26bfd14c9a40575e/ormsgpack-1.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd", size = 425518, upload-time = "2026-01-18T20:55:49.556Z" }, + { url = "https://files.pythonhosted.org/packages/7a/0c/9803aa883d18c7ef197213cd2cbf73ba76472a11fe100fb7dab2884edf48/ormsgpack-1.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:d024b40828f1dde5654faebd0d824f9cc29ad46891f626272dd5bfd7af2333a4", size = 117462, upload-time = "2026-01-18T20:55:47.726Z" }, + { url = "https://files.pythonhosted.org/packages/c8/9e/029e898298b2cc662f10d7a15652a53e3b525b1e7f07e21fef8536a09bb8/ormsgpack-1.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:da538c542bac7d1c8f3f2a937863dba36f013108ce63e55745941dda4b75dbb6", size = 111559, upload-time = "2026-01-18T20:55:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/eb/29/bb0eba3288c0449efbb013e9c6f58aea79cf5cb9ee1921f8865f04c1a9d7/ormsgpack-1.12.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5ea60cb5f210b1cfbad8c002948d73447508e629ec375acb82910e3efa8ff355", size = 378661, upload-time = "2026-01-18T20:55:57.765Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/5efa31346affdac489acade2926989e019e8ca98129658a183e3add7af5e/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1", size = 203194, upload-time = "2026-01-18T20:56:08.252Z" }, + { url = "https://files.pythonhosted.org/packages/eb/56/d0087278beef833187e0167f8527235ebe6f6ffc2a143e9de12a98b1ce87/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29a9f17a3dac6054c0dce7925e0f4995c727f7c41859adf9b5572180f640d172", size = 210778, upload-time = "2026-01-18T20:55:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a2/072343e1413d9443e5a252a8eb591c2d5b1bffbe5e7bfc78c069361b92eb/ormsgpack-1.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39c1bd2092880e413902910388be8715f70b9f15f20779d44e673033a6146f2d", size = 212592, upload-time = "2026-01-18T20:55:32.747Z" }, + { url = "https://files.pythonhosted.org/packages/a2/8b/a0da3b98a91d41187a63b02dda14267eefc2a74fcb43cc2701066cf1510e/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50b7249244382209877deedeee838aef1542f3d0fc28b8fe71ca9d7e1896a0d7", size = 387164, upload-time = "2026-01-18T20:55:40.853Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/6d226bc4cf9fc20d8eb1d976d027a3f7c3491e8f08289a2e76abe96a65f3/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5af04800d844451cf102a59c74a841324868d3f1625c296a06cc655c542a6685", size = 482516, upload-time = "2026-01-18T20:55:42.033Z" }, + { url = "https://files.pythonhosted.org/packages/fb/f1/bb2c7223398543dedb3dbf8bb93aaa737b387de61c5feaad6f908841b782/ormsgpack-1.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cec70477d4371cd524534cd16472d8b9cc187e0e3043a8790545a9a9b296c258", size = 425539, upload-time = "2026-01-18T20:55:24.727Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e8/0fb45f57a2ada1fed374f7494c8cd55e2f88ccd0ab0a669aa3468716bf5f/ormsgpack-1.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:21f4276caca5c03a818041d637e4019bc84f9d6ca8baa5ea03e5cc8bf56140e9", size = 117459, upload-time = "2026-01-18T20:55:56.876Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d4/0cfeea1e960d550a131001a7f38a5132c7ae3ebde4c82af1f364ccc5d904/ormsgpack-1.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:baca4b6773d20a82e36d6fd25f341064244f9f86a13dead95dd7d7f996f51709", size = 111577, upload-time = "2026-01-18T20:55:43.605Z" }, + { url = "https://files.pythonhosted.org/packages/94/16/24d18851334be09c25e87f74307c84950f18c324a4d3c0b41dabdbf19c29/ormsgpack-1.12.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:bc68dd5915f4acf66ff2010ee47c8906dc1cf07399b16f4089f8c71733f6e36c", size = 378717, upload-time = "2026-01-18T20:55:26.164Z" }, + { url = "https://files.pythonhosted.org/packages/b5/a2/88b9b56f83adae8032ac6a6fa7f080c65b3baf9b6b64fd3d37bd202991d4/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46d084427b4132553940070ad95107266656cb646ea9da4975f85cb1a6676553", size = 203183, upload-time = "2026-01-18T20:55:18.815Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/43e4555963bf602e5bdc79cbc8debd8b6d5456c00d2504df9775e74b450b/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c010da16235806cf1d7bc4c96bf286bfa91c686853395a299b3ddb49499a3e13", size = 210814, upload-time = "2026-01-18T20:55:33.973Z" }, + { url = "https://files.pythonhosted.org/packages/78/e1/7cfbf28de8bca6efe7e525b329c31277d1b64ce08dcba723971c241a9d60/ormsgpack-1.12.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18867233df592c997154ff942a6503df274b5ac1765215bceba7a231bea2745d", size = 212634, upload-time = "2026-01-18T20:55:28.634Z" }, + { url = "https://files.pythonhosted.org/packages/95/f8/30ae5716e88d792a4e879debee195653c26ddd3964c968594ddef0a3cc7e/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b009049086ddc6b8f80c76b3955df1aa22a5fbd7673c525cd63bf91f23122ede", size = 387139, upload-time = "2026-01-18T20:56:02.013Z" }, + { url = "https://files.pythonhosted.org/packages/dc/81/aee5b18a3e3a0e52f718b37ab4b8af6fae0d9d6a65103036a90c2a8ffb5d/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1dcc17d92b6390d4f18f937cf0b99054824a7815818012ddca925d6e01c2e49e", size = 482578, upload-time = "2026-01-18T20:55:35.117Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/71c9ba472d5d45f7546317f467a5fc941929cd68fb32796ca3d13dcbaec2/ormsgpack-1.12.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f04b5e896d510b07c0ad733d7fce2d44b260c5e6c402d272128f8941984e4285", size = 425539, upload-time = "2026-01-18T20:56:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/2e/a6/ac99cd7fe77e822fed5250ff4b86fa66dd4238937dd178d2299f10b69816/ormsgpack-1.12.2-cp314-cp314-win_amd64.whl", hash = "sha256:ae3aba7eed4ca7cb79fd3436eddd29140f17ea254b91604aa1eb19bfcedb990f", size = 117493, upload-time = "2026-01-18T20:56:07.343Z" }, + { url = "https://files.pythonhosted.org/packages/3a/67/339872846a1ae4592535385a1c1f93614138566d7af094200c9c3b45d1e5/ormsgpack-1.12.2-cp314-cp314-win_arm64.whl", hash = "sha256:118576ea6006893aea811b17429bfc561b4778fad393f5f538c84af70b01260c", size = 111579, upload-time = "2026-01-18T20:55:21.161Z" }, + { url = "https://files.pythonhosted.org/packages/49/c2/6feb972dc87285ad381749d3882d8aecbde9f6ecf908dd717d33d66df095/ormsgpack-1.12.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7121b3d355d3858781dc40dafe25a32ff8a8242b9d80c692fd548a4b1f7fd3c8", size = 378721, upload-time = "2026-01-18T20:55:52.12Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9a/900a6b9b413e0f8a471cf07830f9cf65939af039a362204b36bd5b581d8b/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ee766d2e78251b7a63daf1cddfac36a73562d3ddef68cacfb41b2af64698033", size = 203170, upload-time = "2026-01-18T20:55:44.469Z" }, + { url = "https://files.pythonhosted.org/packages/87/4c/27a95466354606b256f24fad464d7c97ab62bce6cc529dd4673e1179b8fb/ormsgpack-1.12.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:292410a7d23de9b40444636b9b8f1e4e4b814af7f1ef476e44887e52a123f09d", size = 212816, upload-time = "2026-01-18T20:55:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/73/cd/29cee6007bddf7a834e6cd6f536754c0535fcb939d384f0f37a38b1cddb8/ormsgpack-1.12.2-cp314-cp314t-win_amd64.whl", hash = "sha256:837dd316584485b72ef451d08dd3e96c4a11d12e4963aedb40e08f89685d8ec2", size = 117232, upload-time = "2026-01-18T20:55:45.448Z" }, +] + [[package]] name = "packaging" version = "26.2" @@ -2057,6 +2316,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] +[[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 = "ruff" version = "0.15.13" @@ -2277,6 +2548,93 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] +[[package]] +name = "uuid-utils" +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5a/5da7ae85b38e3eddba0be3e8e4328f90882fe92989728e6fb552963d4c42/uuid_utils-0.16.2.tar.gz", hash = "sha256:fa637e4f314ad5b59ff6d8e809d506443d68bef30bfaecdfcfe02cce689abb2f", size = 42962, upload-time = "2026-06-18T13:36:48.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/96/4023966d42fd9bbf9e2a8ce2b25930113688128b569f68bc4697cb18181d/uuid_utils-0.16.2-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fadd23eee409237fb8637a35796a6e108873c28b40f7de89a36685f18ca055ad", size = 567776, upload-time = "2026-06-18T13:35:02.902Z" }, + { url = "https://files.pythonhosted.org/packages/fc/30/764d2a76e8e7688abd5577e6024787c13692095eb1230fd1936f27205cd9/uuid_utils-0.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:79c5a3bd4301257b9a524efd16baf61ea65cd0d1b60b47d80f20b151fd65a09f", size = 288938, upload-time = "2026-06-18T13:35:04.285Z" }, + { url = "https://files.pythonhosted.org/packages/45/ef/58077250fe04eda4a3f9fba8c35be8d0937b7d3e02302ac1a6d942b77dae/uuid_utils-0.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90903ab7fcdfb0300390c15f5a68cb91f15139d9a1a93f134c783d7a973fa269", size = 327387, upload-time = "2026-06-18T13:35:05.406Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fd/a9172970fa07ce0b9148ccc679a99540375c7bb32f8fbd72cf1e6cca43ef/uuid_utils-0.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7a44f8250ec178c0af703c3f1b6e81865a771272ae735ca403f27c95c62f132", size = 334212, upload-time = "2026-06-18T13:35:06.611Z" }, + { url = "https://files.pythonhosted.org/packages/ca/58/d8fb393b44ad0b719d96a5b7809d0ee727f7e266d9e88a4da235cbfbf9f8/uuid_utils-0.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e97ab941660f781a8e45f15aba9ee01b40dbb96adb5c43617c1671a4604b25b", size = 450379, upload-time = "2026-06-18T13:35:07.97Z" }, + { url = "https://files.pythonhosted.org/packages/00/70/b3cf708e8942e6494742404a66f1586195a20c8fd235bdc79f385db383f1/uuid_utils-0.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a30b6a5790acb854e4b65fae7875e5d3c6f8076fa9c91dac43ff9e28380bc52", size = 327231, upload-time = "2026-06-18T13:35:09.327Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9c/4c5b16e752a2402259a3a9d1371227025e5b85182024c82a446cbe3ed6ea/uuid_utils-0.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5dfc3e9e75139a84898771d31958ece6cdee8e8f127700aa8aa26a4f1a348d57", size = 353455, upload-time = "2026-06-18T13:35:10.67Z" }, + { url = "https://files.pythonhosted.org/packages/0c/0f/3b14c47fab1544bcfb92d28bc468160a4fc6ff342d0e6defa8ff40d5e4bd/uuid_utils-0.16.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0529b1ef0788f663e1211d221b59a38ec67f9b084f1ea5342ba84358b3d87e98", size = 504028, upload-time = "2026-06-18T13:35:12.006Z" }, + { url = "https://files.pythonhosted.org/packages/39/79/1a133214626eb0e18c51ef196946b1263d65b578ffee432ad1b7afffa5d3/uuid_utils-0.16.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:cae08df8695f4b01fce2a8ab50e9e310971276d85dfc7103e977bed52d365094", size = 609803, upload-time = "2026-06-18T13:35:13.286Z" }, + { url = "https://files.pythonhosted.org/packages/87/49/22bff932af63764b4cad9c01299ed64c60d101962988efc13964b4165345/uuid_utils-0.16.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f69658c42411540cf58be958a47e317fd2302cc0b613ea5cff1e60d87be2846d", size = 569512, upload-time = "2026-06-18T13:35:14.661Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/371cc1f332f7463b9cfac0a66f984af00f4e3ada4a196b20879e35404e8b/uuid_utils-0.16.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:503f020acc7dbeb39c47fa33cf2971cf5960fa11f8394513fac461762a90c556", size = 532855, upload-time = "2026-06-18T13:35:15.99Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/f7ef7f6dabf68021eb6e961c09d16d517ed7587cedfff18969ba7f61798c/uuid_utils-0.16.2-cp311-cp311-win32.whl", hash = "sha256:aab7cdf28a3e2859ca4f40a3e3bf53eb35895039c80d4d8d8c5e15b90346c55c", size = 169971, upload-time = "2026-06-18T13:35:17.294Z" }, + { url = "https://files.pythonhosted.org/packages/75/8b/1e4b51c075eaadd23828b708249374db0bc40146f7b673027942d3383f45/uuid_utils-0.16.2-cp311-cp311-win_amd64.whl", hash = "sha256:71192a59d473f3f638e2a238905046e2942006ad90ac5ec10d578e58ff9a08ce", size = 176464, upload-time = "2026-06-18T13:35:18.459Z" }, + { url = "https://files.pythonhosted.org/packages/d4/71/18a43b6e632adf3cb3cf5db777ea03f9d3b2b259de65de5e41419004c2a1/uuid_utils-0.16.2-cp311-cp311-win_arm64.whl", hash = "sha256:ea175649789f1e93edbf1a0440cab18c9838977703917221777691d8d988d7bf", size = 176056, upload-time = "2026-06-18T13:35:19.826Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/294b72a572218bf6e92355203b832b3356c58a7e1e0b92a034497d15bef9/uuid_utils-0.16.2-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:6f064dc54c6abecb09eb104d953bfb079f3c395e0d6b18899979f852d1083549", size = 560726, upload-time = "2026-06-18T13:35:21.053Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3c/1095b6ab574a7fa69136d47bab5a43f320a8f00a0ecb96059fd49b1747b2/uuid_utils-0.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:dd7aa18db5cc826d482d876a826fee445839701f81f78567e7c74b4458d57a84", size = 288065, upload-time = "2026-06-18T13:35:22.547Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9d/6404d48fe71def0733c9568d96043b2e1945e2e4205c4eb525db3da42ba3/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc25ad320c9b44c2d3ed33aff4f85b0b277bef4ff79b12c01ee58b52ea44be1d", size = 322946, upload-time = "2026-06-18T13:35:23.648Z" }, + { url = "https://files.pythonhosted.org/packages/74/00/8a009762015a134aa04b5451400e0ec9832ccd598ed4845f9aecb0be6299/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0ca752d51d1004caff65fccffd44b32a26cb099b546e0512cfa09facb683d6c", size = 330186, upload-time = "2026-06-18T13:35:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/b7/b0/1613bb98ac11234145aa5bc1de618be536818fef05dec595efb3e2b37097/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8323136bb02355c1b973492ab98b0722206dfdedfb148e4115c35fcdf3889bad", size = 444583, upload-time = "2026-06-18T13:35:25.999Z" }, + { url = "https://files.pythonhosted.org/packages/93/66/83e62c7a152bbbb8b30ac58eaad81f3860ba2fba91a334c50f223f9ce878/uuid_utils-0.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bf8bfdffb22f620635580b17fd178272f30a9841b824b19b935c8db64bf09b6", size = 323064, upload-time = "2026-06-18T13:35:27.356Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/c1b2faaf3a9d7952f321a9fee3ad74e05b25878bd9b7cd6b0398fe77f279/uuid_utils-0.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:61454f2139424a6cff14eca7849c28b3350f261453b74075aa20fe99592dbb16", size = 347967, upload-time = "2026-06-18T13:35:28.538Z" }, + { url = "https://files.pythonhosted.org/packages/24/d8/cdf79b242e41ae47b7cd617ac5d48f15ce44e81da8000379c757091ae5f8/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:725110434a1d482a639a9ac467a24f1cb531d84ab52e454a13fe145b10b42cae", size = 499187, upload-time = "2026-06-18T13:35:30.042Z" }, + { url = "https://files.pythonhosted.org/packages/be/10/978d5ad82bc0fe7ff02d5be6f1eb83b090849f0a95bf8438593565273b7a/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8197870739a3094990743a80f075fa0b17beafd6c187e5f360e021d90a12a6d1", size = 605696, upload-time = "2026-06-18T13:35:31.289Z" }, + { url = "https://files.pythonhosted.org/packages/3a/28/e382ee44a592e35b80397b493bf3fbbdb8e30a64eaaefc7dabc246aeb253/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e10a02b3a31ed44c7c9a96abde335f5fa222735e73f3081d693414377eb3b016", size = 564975, upload-time = "2026-06-18T13:35:32.419Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d0/f6011dbe4e5d751a8494715e014019cb5b242d8cd6dbec1cfec3d3fb2e81/uuid_utils-0.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd32dbca0792b9683160151dc07fad11b915020eed7c82b43faf0862c2ff06a0", size = 528462, upload-time = "2026-06-18T13:35:33.685Z" }, + { url = "https://files.pythonhosted.org/packages/42/7f/279e6159c37f43feb9dd70218b49a26696cefddaef1db7f4b79895eaf5d5/uuid_utils-0.16.2-cp312-cp312-win32.whl", hash = "sha256:dcdfcab60562d12dd43c1a6f495b1d089e41f0e10fac37d94db285d72b678c23", size = 167047, upload-time = "2026-06-18T13:35:34.862Z" }, + { url = "https://files.pythonhosted.org/packages/47/38/f72f7bed062601448ec2db47351e6c1faccd78fd693bbc6e067299d1fa11/uuid_utils-0.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:97ee6f5e803ea571f5f6da42efc97d8c5a13f121043680177f8470529b94e855", size = 173821, upload-time = "2026-06-18T13:35:36.117Z" }, + { url = "https://files.pythonhosted.org/packages/37/61/8a025284a31c85b7c0c5319e96868c2c09dea3fc5f676c979a4cd4baf2e7/uuid_utils-0.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:72cfd9ff1e8a7c371a044687e77eb873721c4a9f4814e453439bfba595b84303", size = 172206, upload-time = "2026-06-18T13:35:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a1/3b48859953ee74fc26628ca5d9e5f848209655a0a8c934032fc596035976/uuid_utils-0.16.2-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c19b7d595d12923da682ed13d313c2333b9ebf214e65a47a24927a8a3a81b191", size = 560753, upload-time = "2026-06-18T13:35:38.531Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1c/77635489de5454f2a25411030f78d31931dbdc0c86114da00adb9b91f120/uuid_utils-0.16.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:909e26fa2451c8db31b9ed1d3c8e4ecf513b6d1619db4205997fe99eb6b4ef4f", size = 288056, upload-time = "2026-06-18T13:35:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0e/8e799537ea458abaefb0f5c3b3b05304d3faf413feb0997605a3f8ae2484/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27271b37fbc6812bb1542c4b8e22ee00223a6bf7f62b1f38d3bcf8e92f6d9acd", size = 323196, upload-time = "2026-06-18T13:35:41.534Z" }, + { url = "https://files.pythonhosted.org/packages/e8/92/4e5b412d4710617fb83ed77b361f5fa6247b99bde2fa6ee07ddf851b59d1/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc4b9d96a2c689d664cf3fc7f7db46b82d2821fb2ce8a4f0798fc0a92c1569f8", size = 330858, upload-time = "2026-06-18T13:35:42.709Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e3/8173202b7cfcfeb4a588c5f8b85d3e2b44973384eb33167ee25c5c78867f/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3bf41b696b0fe808df1b4091c70273a52ea033b0fe97341cd67ecd76d22bb3a", size = 444813, upload-time = "2026-06-18T13:35:43.917Z" }, + { url = "https://files.pythonhosted.org/packages/37/0d/c3918356932ce467b11e954d0c93697fb4652cf664957e3d9521f7ece22f/uuid_utils-0.16.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcc329be41bb6534ecb03e50596179ab76c7643ced33d13c66967d5ae1869663", size = 322828, upload-time = "2026-06-18T13:35:45.134Z" }, + { url = "https://files.pythonhosted.org/packages/f0/80/4020556682441b62a25b7d07798812115fca97d417a3498d5af6dce36504/uuid_utils-0.16.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4125bf6ed3ae443c05e140f8585d174b9d647295b12034d5ec94ae2ae38edefa", size = 347909, upload-time = "2026-06-18T13:35:46.364Z" }, + { url = "https://files.pythonhosted.org/packages/48/2f/a1e87e268df98f6740af81abf225532c173a971c64df0258c84b630e35a7/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:840b21e609a9b203eee06bdc73e18397154447a9814a8e78d9b68e5104d9802f", size = 499469, upload-time = "2026-06-18T13:35:47.584Z" }, + { url = "https://files.pythonhosted.org/packages/25/75/5a1f297a09556c27d9617c44ab0510de5f3a70120df236f66b9d0fdd1976/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:5119bec75f56bd028d97472f72b1ed723a0d60b09a48017dc70a3cb1892ed081", size = 606160, upload-time = "2026-06-18T13:35:48.963Z" }, + { url = "https://files.pythonhosted.org/packages/7c/de/140f1d2a161320d1ac9073a03b9eb31fe35ae70f56f8971ec1fb45c14a44/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9fe600ab7d3d4eb56986e814042c917e728ac92cd8a41f099a6b59b84d8bf9e6", size = 564856, upload-time = "2026-06-18T13:35:50.244Z" }, + { url = "https://files.pythonhosted.org/packages/01/3b/9a5fe6691f8f6d72899cdc2713ffbd845b8c6981eeeab66d98a71b721116/uuid_utils-0.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44020a4532229ccfbba353138539774686350dda71cf4368e257973dd8ba403", size = 528376, upload-time = "2026-06-18T13:35:51.825Z" }, + { url = "https://files.pythonhosted.org/packages/87/ad/47c93dcabd00f6749803a00be361c75d7079c78ad5e67077dee63d30b687/uuid_utils-0.16.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:280d4f1f22dd2e79c1cc31ffc7fc26dc3534ffc114dedcdd29cc8489c5ce9c98", size = 98033, upload-time = "2026-06-18T13:35:53.385Z" }, + { url = "https://files.pythonhosted.org/packages/c0/fd/8de85eeb8dd59354ad46e897ab0d0f0fe6bc48702239a6c9f2613f961c8e/uuid_utils-0.16.2-cp313-cp313-win32.whl", hash = "sha256:4942b26ad12c5187bac52b7fb4685040139ff0df9a19cde33e5025326f6180fc", size = 167054, upload-time = "2026-06-18T13:35:54.495Z" }, + { url = "https://files.pythonhosted.org/packages/86/b3/b5ba393fbe5142eb9d5db23d4b9b16dde2a4e1aee6f2fcb7fadef97e419a/uuid_utils-0.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:01f81c71cf2185de0707e9d2f248e17025ba50af0acd3cbf51cd8aea96c2e0be", size = 173481, upload-time = "2026-06-18T13:35:55.684Z" }, + { url = "https://files.pythonhosted.org/packages/b2/79/4e5d63d605b13201ae9af6fcc36ec77949cccc99486c430c016d8f8ed274/uuid_utils-0.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:c1dbe65ce6d46c5f645356d64bfb2de7564e2426ca8c9b1a0a401d6f7ae5cc22", size = 172197, upload-time = "2026-06-18T13:35:56.817Z" }, + { url = "https://files.pythonhosted.org/packages/89/3a/0e5a0c1e1e3243cf5f12efd2b88a33e63c38b6a79483d3c84b2f5e7265cf/uuid_utils-0.16.2-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:617955f4b3f649617c0388127d8a257202189d5cc3c720313f8b207df1cdb2a4", size = 566227, upload-time = "2026-06-18T13:35:57.925Z" }, + { url = "https://files.pythonhosted.org/packages/28/b3/2b6f9d6832e939aaf2b2ba89ff70b3994cfa3ae9b14daac3329eb9202ef8/uuid_utils-0.16.2-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:0aa2569908bdb21ccb216cd6bd06cb934351ee65ea7cd5e351e19f633a99b577", size = 290301, upload-time = "2026-06-18T13:35:59.467Z" }, + { url = "https://files.pythonhosted.org/packages/f5/27/8bb31429884b9f340f964ed70b68bfd81cec61f6e6877633f6a014358e78/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4af7673e84e1ec6029f18d3a0408095c471c4e2691b6e46b4e1f0a2051734ba", size = 325409, upload-time = "2026-06-18T13:36:00.786Z" }, + { url = "https://files.pythonhosted.org/packages/1f/87/3b59aa97e788ca4fa46e2a3856ef567b51e03fd7fbf27d39ce36e46478b6/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ecadf55ed6b8fb72e7966b52fd02919e7d7bb8e7bffeaf285803b82e774debfb", size = 332071, upload-time = "2026-06-18T13:36:02.043Z" }, + { url = "https://files.pythonhosted.org/packages/1c/21/8c21bf6cf3ce9447b73cee6a38ca63c9bb2f3145259422646bae8e8ddc21/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:026b96b2f1e6b004579e030692d2f6568ccd0b29d40687213c31694abf570c78", size = 447075, upload-time = "2026-06-18T13:36:03.305Z" }, + { url = "https://files.pythonhosted.org/packages/95/43/77e83019effe1a5ab7169a2d4bf1bd654bebd850b81c8a937b96bd6b5c9c/uuid_utils-0.16.2-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:273679723e88544dd2de0564ab7f2fddfa2270faf05cabfdf63c275be67ec2a1", size = 325061, upload-time = "2026-06-18T13:36:04.972Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a6/7bf6e0165dc191c09bc4e8c011de5463d64c5a651ed38ad6698bfc552a52/uuid_utils-0.16.2-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec5b1a338b92d1eb121e9eaf06ae3db1b9a5cd794ce318a475f6dc6f9e89c3a8", size = 350302, upload-time = "2026-06-18T13:36:06.172Z" }, + { url = "https://files.pythonhosted.org/packages/45/66/260836aaef14b8254bc449b3163fedec06ef0a0bba0d6a999c918479b2f9/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e75f9429d4533ce275c98bc68bf47fb237ae7b32c954266dabc5edab0c7d682e", size = 501834, upload-time = "2026-06-18T13:36:07.469Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/84c1542bf8c465b456f742318ad83eace63551e7f603b06c817b726670af/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f3cca9ca5e2c2dfd7b885f0d34c10b993a070d3593f3cdfef785195da36fb0f", size = 607406, upload-time = "2026-06-18T13:36:08.913Z" }, + { url = "https://files.pythonhosted.org/packages/48/7f/1024c22657a0c0572c4fd5189fad3127cb46731fb26fad3be1e8a4a64972/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1ef8c561fdf88fec205e3d54037824cfe2addce16b509a8d2ecb69daa904cbb7", size = 567623, upload-time = "2026-06-18T13:36:10.14Z" }, + { url = "https://files.pythonhosted.org/packages/15/0e/ad7424a6444e3e108a22781c2e164e82752da5db23ccc5cba8b4470c3164/uuid_utils-0.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3e3acb5e1451232381daea01645a98c69de4bb9ad88d77a1f7c1df4d83d54e62", size = 530659, upload-time = "2026-06-18T13:36:11.649Z" }, + { url = "https://files.pythonhosted.org/packages/69/60/cf1666d0dbd6fa869b6de3b85a17254ff0ab10ed286fd59366148bf08e89/uuid_utils-0.16.2-cp314-cp314-win32.whl", hash = "sha256:b5f8e7d0bb2c6e6180176237f92d2e949626e04fcf701c49d73f128e1f64e1d1", size = 169272, upload-time = "2026-06-18T13:36:12.846Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5e/111908bdc7287b2589e9a9f10be8e0358844fb4a0554677cbbe0ade49766/uuid_utils-0.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:bf922bad7df257336b594d316a1657df569860bb5389602919001fa6fb17f06e", size = 175435, upload-time = "2026-06-18T13:36:14.114Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5d/b3bd7415622060dd17d587545e3c037f83dc0dffb8880ac798ca7936f630/uuid_utils-0.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:fad82e6482129c58ba9b00da6c247ab6e767645ab17981599229cce19d7b2ce9", size = 173553, upload-time = "2026-06-18T13:36:15.561Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/401acf6fc0e0665dd11a095a28f6d22708c6f8f148c326cfc5b0b1ae9882/uuid_utils-0.16.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e0609e7e906c08386b7f33141254df05dcab24f1c4884150988dc7a287516aca", size = 567548, upload-time = "2026-06-18T13:36:16.848Z" }, + { url = "https://files.pythonhosted.org/packages/4e/2c/cc2bb8273d414d651acafccc3705a8843c130a541fcce65fbeaac22266ba/uuid_utils-0.16.2-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:9ad2adeb941292fe02e1e5c70b80a5746c45b1b77594506c2a1421455d8384f9", size = 291348, upload-time = "2026-06-18T13:36:18.145Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a8/fdadd7ada0de53dbc03f719da0948cc275abd24d8013a26e42e50d3665c1/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d906c00f965d5c5f4812d0086dc49bf813285ea84c97e8816405200e146f805b", size = 325495, upload-time = "2026-06-18T13:36:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/16/42/e397a1eda06b20dd3a206e3a55b346ff2caad23906586801a87359530864/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a59205fc15463dd0f978f14df14307737e3d4e8ef4aefa29a9d0fa766d84d16b", size = 332301, upload-time = "2026-06-18T13:36:20.747Z" }, + { url = "https://files.pythonhosted.org/packages/46/be/12d3df7bd824e3ce71630c022184a5aecfea92b0a7fa70459542b237777a/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac82500329ffaf2788dac36cf133e1e4e23b6d5e1118274ea6749c3b512f4f1", size = 446760, upload-time = "2026-06-18T13:36:22.198Z" }, + { url = "https://files.pythonhosted.org/packages/f7/10/0c5d1dd6874fa35e2cb66a8499ce303eb8678bef226951182603bd30017d/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d8257329f26905f009aed694bd3b17f334f43748b03134dc7bc99d6c5b4e371", size = 325781, upload-time = "2026-06-18T13:36:23.566Z" }, + { url = "https://files.pythonhosted.org/packages/04/e2/9ebb8414875e5c14737fa7145a023458c9b15754f1d129cefe7824197256/uuid_utils-0.16.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e04b5c10c6fcf9d9801084d1e86c9d7ada7eb48fe07ee4ae5e7fe5b1a852db8a", size = 351189, upload-time = "2026-06-18T13:36:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/1b/5c/168d1f4d30b33c08365debfe4176c2f713a0940f1f11a64128a186d050c6/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3d4805c4739dd06d539f8f4fa94f5aaf26eca4b3ece1ef134d4ff904c6b08dcf", size = 501866, upload-time = "2026-06-18T13:36:26.31Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8d/003865d5ed5bf82ece80bd61edb2692985f7548051749fd10f34edb16705/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:76632d2e16e26de777851ec07961ceaea14e65167d0603a0b17fb169fa9ca37b", size = 607632, upload-time = "2026-06-18T13:36:27.704Z" }, + { url = "https://files.pythonhosted.org/packages/ea/52/6102f21f28323b27122a6aa3d4cea183b4fc401868c5c40767e1b9f53beb/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:6c02f85f49c9c2abbf247a8622458c30232332a28711755aa191da5f38015af6", size = 568216, upload-time = "2026-06-18T13:36:29.377Z" }, + { url = "https://files.pythonhosted.org/packages/68/50/644e4e55f47048d12bc20665fac85bc1fecbed9c892acfb91626abf8ad8d/uuid_utils-0.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f668035ea9faa763e8f1ea42040e8439db88cf2517056d47c348a62a257a1d02", size = 531370, upload-time = "2026-06-18T13:36:30.804Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5d/d98d99f601d70cc00287dce5aadef9c199912f0d64343962542f35e7db59/uuid_utils-0.16.2-cp314-cp314t-win32.whl", hash = "sha256:62b8841895eff1c0afbaf5f0050411667231160478c8ff9f411742abffd3b619", size = 169424, upload-time = "2026-06-18T13:36:32.246Z" }, + { url = "https://files.pythonhosted.org/packages/a6/af/c0d482bdd637a8a742d3274cec462b770919f032e179216f2fc2851afaf9/uuid_utils-0.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:e9064805881c30dd80a4189a0da7130e3d684de353ea36edd99c1b994bdf429e", size = 175544, upload-time = "2026-06-18T13:36:33.75Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/aff8b0456e8a63672fa89ea9c773f7547a31ff7b596a40f226bf148921a3/uuid_utils-0.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:3324bac95084e63e28553c92fac5a0394c636a76e03e50a7dab0c0bbddf87fa5", size = 173972, upload-time = "2026-06-18T13:36:35.076Z" }, + { url = "https://files.pythonhosted.org/packages/0e/48/8c9fee7d75571f2f4b2386eac798fe5f826155d13797f7c86d45eb3fdc23/uuid_utils-0.16.2-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8b8e325e61f918caf74ca540e3384b81e6e22aea782e57f615d15fc9773b96c8", size = 571003, upload-time = "2026-06-18T13:36:36.42Z" }, + { url = "https://files.pythonhosted.org/packages/de/78/754eaaa49509be6fdb705de61d1e3889de32002132d5f00e8c1e5d212da3/uuid_utils-0.16.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9282677ebf2ea5b437c20d16e75bcd7629bdc205018f95557b33b76868d8bb5b", size = 290244, upload-time = "2026-06-18T13:36:38.066Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e2/bfcbcf7eb9dfb17701104c569ed771eb359737bc70b7309e439610d089ef/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e9ca7f5e215373cc9c147172170a0b1a4ab0dee9cc62fe446d9b075f31e3241", size = 328551, upload-time = "2026-06-18T13:36:39.605Z" }, + { url = "https://files.pythonhosted.org/packages/72/bf/bbdbc39d1421953edcee0bad13a1893521a636eccf381580f53e530a4feb/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43cc72a92694d08ade8faadacf928857d9cceb84b449473246ae4e4f263d7d22", size = 335468, upload-time = "2026-06-18T13:36:41Z" }, + { url = "https://files.pythonhosted.org/packages/04/2a/e8d4e6f1f2d2e567cf6e3202d125afe7da52ad7680bba048b106c09f01b9/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:511b5fde12d29c37a9badd399af62105bb2f4696aa10eb18be74e7b9ca84413a", size = 450984, upload-time = "2026-06-18T13:36:42.635Z" }, + { url = "https://files.pythonhosted.org/packages/6a/da/ddb1dcf0fe9bfcb0dfcddec8ae52c8f95e7088e44719f58477f5fb2c5586/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:585d3adf73afa60348bf2bd529491c640a692350e76d8ff3974455e273aadfe7", size = 327940, upload-time = "2026-06-18T13:36:44.138Z" }, + { url = "https://files.pythonhosted.org/packages/37/fb/39305fbfffee1fdaccdb88fc0499ac9dcb7289a77ebc31938dcdd933cf95/uuid_utils-0.16.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ae5fa2007fd26d26f7b09e76259d5ca99bec191616207ca929f8dca12da08129", size = 355368, upload-time = "2026-06-18T13:36:45.682Z" }, + { url = "https://files.pythonhosted.org/packages/b4/70/b708edc3b776d7624b4354f43d443f14d951d3ac4d7d8867d94f2e59c3ae/uuid_utils-0.16.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9b4520521aa46a2582fe1829c535fe60b78999b89257db998df3816eb895bdf3", size = 178221, upload-time = "2026-06-18T13:36:47.291Z" }, +] + [[package]] name = "uvicorn" version = "0.47.0" @@ -2454,61 +2812,44 @@ wheels = [ [[package]] name = "websockets" -version = "16.0" +version = "15.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +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/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" }, - { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, - { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, - { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, - { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, - { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" }, - { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" }, - { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" }, - { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, + { 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/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] [[package]] @@ -2519,6 +2860,8 @@ dependencies = [ { name = "alembic" }, { name = "email-validator" }, { name = "fastapi" }, + { name = "jieba" }, + { name = "langgraph" }, { name = "lightrag-hku" }, { name = "openpyxl" }, { name = "psycopg", extra = ["binary"] }, @@ -2547,6 +2890,8 @@ requires-dist = [ { name = "email-validator", specifier = ">=2.2.0,<3.0.0" }, { name = "fastapi", specifier = ">=0.115.0,<1.0.0" }, { name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.0,<1.0.0" }, + { name = "jieba", specifier = ">=0.42.1,<0.43.0" }, + { name = "langgraph", specifier = ">=1.2.0,<2.0.0" }, { name = "lightrag-hku", specifier = ">=1.4.16,<1.5.0" }, { name = "openpyxl", specifier = ">=3.1.5,<4.0.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.0,<4.0.0" }, @@ -2572,6 +2917,144 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, ] +[[package]] +name = "xxhash" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/f4/7bd35089ff1f8e2c96baa2dce05775a122aacd2e3830a73165e27a4d0848/xxhash-3.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fdc7d06929ae28dda98297a18eef7b0fd38991a3b405d8d7b55c9ef24c296958", size = 33423, upload-time = "2026-04-25T11:05:47.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/26/4e00c88a6a2c8a759cfb77d2a9a405f901e8aa66e60ef1fd0aeb35edda48/xxhash-3.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea6daa712f4e094a30830cf01e9b47d03b24d05cc9dab8609f0d9a9db8454712", size = 30857, upload-time = "2026-04-25T11:05:49.189Z" }, + { url = "https://files.pythonhosted.org/packages/82/2f/eeb942c17a5a761a8f01cb9180a0b76bfb62a2c39e6f46b1f9001899027a/xxhash-3.7.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9e6c0d843f1daf85ea23aeb053579135552bde575b7b98af20bfc667b6e4548d", size = 194702, upload-time = "2026-04-25T11:05:50.457Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/96f132c08b1e5951c68691d3b9ec351ec2edc028f6a01fcd294f46b9d9f0/xxhash-3.7.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:363c139bf15e1ac5f136b981d3c077eb551299b1effede7f12faa010b8590a60", size = 213613, upload-time = "2026-04-25T11:05:52.571Z" }, + { url = "https://files.pythonhosted.org/packages/82/89/d4e92b796c5ed052d29ed324dbfc1dc1188e0c4bf64bebbf0f8fc20698df/xxhash-3.7.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a778b25874cb0f862eaab5986bff4ca49ffb0def7c0a34c237b948b3c6c775b2", size = 236726, upload-time = "2026-04-25T11:05:54.395Z" }, + { url = "https://files.pythonhosted.org/packages/40/f1/81fc4361921dc6e557a9c60cb3712f36d244d06eeeb71cd2f4252ac42678/xxhash-3.7.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e1860f1e43d40e9d904cf22d93e587ea42e010ebce4160877e46bcab4bc232a", size = 212443, upload-time = "2026-04-25T11:05:56.334Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d0/afeddd4cff50a332f50d4b8a2e8857673153ab0564ef472fcdeb0b5430df/xxhash-3.7.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9122ad6f867c4a0f5e655f5c3bdf89103852009dbb442a3d23e688b9e699e800", size = 445793, upload-time = "2026-04-25T11:05:58.953Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/3c91e4e6a05ca4d7df8e39ec3a75b713609258ec84705ab34be6430826a1/xxhash-3.7.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d9110d0c3fb02679972837a033251fd186c529aa62f19c132fc909c74052b8", size = 193937, upload-time = "2026-04-25T11:06:00.546Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3a/a6b0772d9801dd4bea4ca4fd34734d6e9b51a711c8a611a24a79de26a878/xxhash-3.7.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:347a93f2b4ce67ce61959665e32a7447c380f8347e55e100daa23766baacf0e5", size = 285188, upload-time = "2026-04-25T11:06:01.96Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f8/cf8e31fd7282230fe7367cd501a2e75b4b67b222bfc7eacccfc20d2652cb/xxhash-3.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:acbb48679ddf3852c45280c10ff10d52ca2cd1da2e552fb81db1ff786c75d0e4", size = 210966, upload-time = "2026-04-25T11:06:03.453Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f0/fd36cc4a81bf52ee5633275daae2b93dd958aace67fd4f5d466ec83b5f35/xxhash-3.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:fe14c356f8b23ad811dc026077a6d4abccdaa7bce5ca98579605550657b6fcfb", size = 241994, upload-time = "2026-04-25T11:06:05.264Z" }, + { url = "https://files.pythonhosted.org/packages/08/e1/67f5d9c9369be42eaf99ba02c01bf14c5ecd67087b02567960bfcee43b63/xxhash-3.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f420ad3d41e38194353a498bbc9561fd5a9973a27b536ce46d8583479cf44335", size = 198707, upload-time = "2026-04-25T11:06:07.044Z" }, + { url = "https://files.pythonhosted.org/packages/50/17/a4c865ca22d2da6b1bc7d739bf88cab209533cf52ba06ca9da27c3039bee/xxhash-3.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:693d02c6dc7d1aa0a45921d54cd8c1ff629e09dfdc2238471507af1f7a1c6f04", size = 210917, upload-time = "2026-04-25T11:06:08.853Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/453b35810d697abac3c96bde3528bece685869227da274eb80a4a4d4a119/xxhash-3.7.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:14bf7a54e43825ec131ee7fe3c60e142e7c2c1e676ad0f93fc893432d15414af", size = 275772, upload-time = "2026-04-25T11:06:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/b5/ad/4eed7eab07fd3ee6678f416190f0413d097ab5d7c1278906bf1e9549d789/xxhash-3.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ae3a39a4d96bdb6f8d154fd7f490c4ad06f0532fcd2bb656052a9a7762cf5d31", size = 414068, upload-time = "2026-04-25T11:06:12.511Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4e/fd6f8a680ba248fdb83054fa71a8bfa3891225200de1708b888ef2c49829/xxhash-3.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1cc07c639e3a77ef1d32987464d3e408565b8a3be57b545d3542b191054d9923", size = 191459, upload-time = "2026-04-25T11:06:14.07Z" }, + { url = "https://files.pythonhosted.org/packages/50/7c/8cb34b3bed4f44ca6827a534d50833f9bc6c006e83b0eb410ac9fa0793bd/xxhash-3.7.0-cp311-cp311-win32.whl", hash = "sha256:3281ba1d1e60ee7a382a7b958513ba03c2c0d5fcbd9a6f7517c0a81251a23422", size = 30628, upload-time = "2026-04-25T11:06:15.802Z" }, + { url = "https://files.pythonhosted.org/packages/0b/47/a49767bd7b40782bedae9ff0721bfe1d7e4dd9dc1585dea684e57ba67c20/xxhash-3.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:a7f25baec4c5d851d40718d6fae52285b31683093d4ff5207e63ab306ccf14a5", size = 31461, upload-time = "2026-04-25T11:06:17.104Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c6/3957bfacfb706bd687be246dfa8dd60f8df97c44186d229f7fd6e26c4b7e/xxhash-3.7.0-cp311-cp311-win_arm64.whl", hash = "sha256:4c2454448ce847c72635827bb75c15c5a3434b03ee1afd28cb6dc6fb2597d830", size = 27746, upload-time = "2026-04-25T11:06:18.716Z" }, + { url = "https://files.pythonhosted.org/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a", size = 33357, upload-time = "2026-04-25T11:06:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0", size = 194100, upload-time = "2026-04-25T11:06:23.586Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" }, + { url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386", size = 197950, upload-time = "2026-04-25T11:06:39.148Z" }, + { url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81", size = 30671, upload-time = "2026-04-25T11:06:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1", size = 31514, upload-time = "2026-04-25T11:06:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852", size = 27761, upload-time = "2026-04-25T11:06:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" }, + { url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" }, + { url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" }, + { url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" }, + { url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" }, + { url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" }, + { url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" }, + { url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" }, + { url = "https://files.pythonhosted.org/packages/54/c1/e57ac7317b1f58a92bab692da6d497e2a7ce44735b224e296347a7ecc754/xxhash-3.7.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad3aa71e12ee634f22b39a0ff439357583706e50765f17f05550f92dbf128a23", size = 31232, upload-time = "2026-04-25T11:10:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4e/075559bd712bc62e84915ea46bbee859f935d285659082c129bdbff679dd/xxhash-3.7.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5de686e73690cdaf72b96d4fa083c230ec9020bcc2627ce6316138e2cf2fe2d1", size = 28553, upload-time = "2026-04-25T11:10:23.1Z" }, + { url = "https://files.pythonhosted.org/packages/92/ca/a9c78cb384d4b033b0c58196bd5c8509873cabe76389e195127b0302a741/xxhash-3.7.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7fbec49f5341bbdea0c471f7d1e2fb41ae8925af9b6f28025c28defd8eb94274", size = 41109, upload-time = "2026-04-25T11:10:25.022Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b1/dfe2629f7c77eb2fa234c72ff537cdd64939763df704e256446ed364a16d/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48b542c347c2089f43dc5a6db31d2a6f3cdb04ee33505ec6e9f653834dbb0bde", size = 36307, upload-time = "2026-04-25T11:10:26.949Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f7/5a484afce0f48dd8083208b42e4911f290a82c7b52458ef2927e4d421a45/xxhash-3.7.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a169a036bed0995e090d1493b283cc2cc8a6f5046821086b843abefff80643bc", size = 32534, upload-time = "2026-04-25T11:10:29.01Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5f/4acfcd490db9780cf36c58534d828003c564cde5350220a1c783c4d10776/xxhash-3.7.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:ec101643395d7f21405b640f728f6f627e6986557027d740f2f9b220955edafe", size = 31552, upload-time = "2026-04-25T11:10:30.727Z" }, +] + [[package]] name = "yarl" version = "1.24.2" @@ -2670,3 +3153,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, ] + +[[package]] +name = "zstandard" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/83/c3ca27c363d104980f1c9cee1101cc8ba724ac8c28a033ede6aab89585b1/zstandard-0.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:933b65d7680ea337180733cf9e87293cc5500cc0eb3fc8769f4d3c88d724ec5c", size = 795254, upload-time = "2025-09-14T22:16:26.137Z" }, + { url = "https://files.pythonhosted.org/packages/ac/4d/e66465c5411a7cf4866aeadc7d108081d8ceba9bc7abe6b14aa21c671ec3/zstandard-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3f79487c687b1fc69f19e487cd949bf3aae653d181dfb5fde3bf6d18894706f", size = 640559, upload-time = "2025-09-14T22:16:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/354fe655905f290d3b147b33fe946b0f27e791e4b50a5f004c802cb3eb7b/zstandard-0.25.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:0bbc9a0c65ce0eea3c34a691e3c4b6889f5f3909ba4822ab385fab9057099431", size = 5348020, upload-time = "2025-09-14T22:16:29.523Z" }, + { url = "https://files.pythonhosted.org/packages/3b/13/2b7ed68bd85e69a2069bcc72141d378f22cae5a0f3b353a2c8f50ef30c1b/zstandard-0.25.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:01582723b3ccd6939ab7b3a78622c573799d5d8737b534b86d0e06ac18dbde4a", size = 5058126, upload-time = "2025-09-14T22:16:31.811Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/fdaf0674f4b10d92cb120ccff58bbb6626bf8368f00ebfd2a41ba4a0dc99/zstandard-0.25.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5f1ad7bf88535edcf30038f6919abe087f606f62c00a87d7e33e7fc57cb69fcc", size = 5405390, upload-time = "2025-09-14T22:16:33.486Z" }, + { url = "https://files.pythonhosted.org/packages/0f/67/354d1555575bc2490435f90d67ca4dd65238ff2f119f30f72d5cde09c2ad/zstandard-0.25.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:06acb75eebeedb77b69048031282737717a63e71e4ae3f77cc0c3b9508320df6", size = 5452914, upload-time = "2025-09-14T22:16:35.277Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/e9cfd801a3f9190bf3e759c422bbfd2247db9d7f3d54a56ecde70137791a/zstandard-0.25.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9300d02ea7c6506f00e627e287e0492a5eb0371ec1670ae852fefffa6164b072", size = 5559635, upload-time = "2025-09-14T22:16:37.141Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/5ba550f797ca953a52d708c8e4f380959e7e3280af029e38fbf47b55916e/zstandard-0.25.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfd06b1c5584b657a2892a6014c2f4c20e0db0208c159148fa78c65f7e0b0277", size = 5048277, upload-time = "2025-09-14T22:16:38.807Z" }, + { url = "https://files.pythonhosted.org/packages/46/c0/ca3e533b4fa03112facbe7fbe7779cb1ebec215688e5df576fe5429172e0/zstandard-0.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f373da2c1757bb7f1acaf09369cdc1d51d84131e50d5fa9863982fd626466313", size = 5574377, upload-time = "2025-09-14T22:16:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/12/9b/3fb626390113f272abd0799fd677ea33d5fc3ec185e62e6be534493c4b60/zstandard-0.25.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c0e5a65158a7946e7a7affa6418878ef97ab66636f13353b8502d7ea03c8097", size = 4961493, upload-time = "2025-09-14T22:16:43.3Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d3/23094a6b6a4b1343b27ae68249daa17ae0651fcfec9ed4de09d14b940285/zstandard-0.25.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c8e167d5adf59476fa3e37bee730890e389410c354771a62e3c076c86f9f7778", size = 5269018, upload-time = "2025-09-14T22:16:45.292Z" }, + { url = "https://files.pythonhosted.org/packages/8c/a7/bb5a0c1c0f3f4b5e9d5b55198e39de91e04ba7c205cc46fcb0f95f0383c1/zstandard-0.25.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:98750a309eb2f020da61e727de7d7ba3c57c97cf6213f6f6277bb7fb42a8e065", size = 5443672, upload-time = "2025-09-14T22:16:47.076Z" }, + { url = "https://files.pythonhosted.org/packages/27/22/503347aa08d073993f25109c36c8d9f029c7d5949198050962cb568dfa5e/zstandard-0.25.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:22a086cff1b6ceca18a8dd6096ec631e430e93a8e70a9ca5efa7561a00f826fa", size = 5822753, upload-time = "2025-09-14T22:16:49.316Z" }, + { url = "https://files.pythonhosted.org/packages/e2/be/94267dc6ee64f0f8ba2b2ae7c7a2df934a816baaa7291db9e1aa77394c3c/zstandard-0.25.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:72d35d7aa0bba323965da807a462b0966c91608ef3a48ba761678cb20ce5d8b7", size = 5366047, upload-time = "2025-09-14T22:16:51.328Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a3/732893eab0a3a7aecff8b99052fecf9f605cf0fb5fb6d0290e36beee47a4/zstandard-0.25.0-cp311-cp311-win32.whl", hash = "sha256:f5aeea11ded7320a84dcdd62a3d95b5186834224a9e55b92ccae35d21a8b63d4", size = 436484, upload-time = "2025-09-14T22:16:55.005Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/c6155f5c1cce691cb80dfd38627046e50af3ee9ddc5d0b45b9b063bfb8c9/zstandard-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:daab68faadb847063d0c56f361a289c4f268706b598afbf9ad113cbe5c38b6b2", size = 506183, upload-time = "2025-09-14T22:16:52.753Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3e/8945ab86a0820cc0e0cdbf38086a92868a9172020fdab8a03ac19662b0e5/zstandard-0.25.0-cp311-cp311-win_arm64.whl", hash = "sha256:22a06c5df3751bb7dc67406f5374734ccee8ed37fc5981bf1ad7041831fa1137", size = 462533, upload-time = "2025-09-14T22:16:53.878Z" }, + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, + { url = "https://files.pythonhosted.org/packages/35/0b/8df9c4ad06af91d39e94fa96cc010a24ac4ef1378d3efab9223cc8593d40/zstandard-0.25.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec996f12524f88e151c339688c3897194821d7f03081ab35d31d1e12ec975e94", size = 795735, upload-time = "2025-09-14T22:17:26.042Z" }, + { url = "https://files.pythonhosted.org/packages/3f/06/9ae96a3e5dcfd119377ba33d4c42a7d89da1efabd5cb3e366b156c45ff4d/zstandard-0.25.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a1a4ae2dec3993a32247995bdfe367fc3266da832d82f8438c8570f989753de1", size = 640440, upload-time = "2025-09-14T22:17:27.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/14/933d27204c2bd404229c69f445862454dcc101cd69ef8c6068f15aaec12c/zstandard-0.25.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:e96594a5537722fdfb79951672a2a63aec5ebfb823e7560586f7484819f2a08f", size = 5343070, upload-time = "2025-09-14T22:17:28.896Z" }, + { url = "https://files.pythonhosted.org/packages/6d/db/ddb11011826ed7db9d0e485d13df79b58586bfdec56e5c84a928a9a78c1c/zstandard-0.25.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bfc4e20784722098822e3eee42b8e576b379ed72cca4a7cb856ae733e62192ea", size = 5063001, upload-time = "2025-09-14T22:17:31.044Z" }, + { url = "https://files.pythonhosted.org/packages/db/00/87466ea3f99599d02a5238498b87bf84a6348290c19571051839ca943777/zstandard-0.25.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:457ed498fc58cdc12fc48f7950e02740d4f7ae9493dd4ab2168a47c93c31298e", size = 5394120, upload-time = "2025-09-14T22:17:32.711Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/fc5531d9c618a679a20ff6c29e2b3ef1d1f4ad66c5e161ae6ff847d102a9/zstandard-0.25.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd7a5004eb1980d3cefe26b2685bcb0b17989901a70a1040d1ac86f1d898c551", size = 5451230, upload-time = "2025-09-14T22:17:34.41Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/e3678b4e776db00f9f7b2fe58e547e8928ef32727d7a1ff01dea010f3f13/zstandard-0.25.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8e735494da3db08694d26480f1493ad2cf86e99bdd53e8e9771b2752a5c0246a", size = 5547173, upload-time = "2025-09-14T22:17:36.084Z" }, + { url = "https://files.pythonhosted.org/packages/4e/d5/ba05ed95c6b8ec30bd468dfeab20589f2cf709b5c940483e31d991f2ca58/zstandard-0.25.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3a39c94ad7866160a4a46d772e43311a743c316942037671beb264e395bdd611", size = 5046736, upload-time = "2025-09-14T22:17:37.891Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/870aa06b3a76c73eced65c044b92286a3c4e00554005ff51962deef28e28/zstandard-0.25.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:172de1f06947577d3a3005416977cce6168f2261284c02080e7ad0185faeced3", size = 5576368, upload-time = "2025-09-14T22:17:40.206Z" }, + { url = "https://files.pythonhosted.org/packages/5d/35/398dc2ffc89d304d59bc12f0fdd931b4ce455bddf7038a0a67733a25f550/zstandard-0.25.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3c83b0188c852a47cd13ef3bf9209fb0a77fa5374958b8c53aaa699398c6bd7b", size = 4954022, upload-time = "2025-09-14T22:17:41.879Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5c/36ba1e5507d56d2213202ec2b05e8541734af5f2ce378c5d1ceaf4d88dc4/zstandard-0.25.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1673b7199bbe763365b81a4f3252b8e80f44c9e323fc42940dc8843bfeaf9851", size = 5267889, upload-time = "2025-09-14T22:17:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/70/e8/2ec6b6fb7358b2ec0113ae202647ca7c0e9d15b61c005ae5225ad0995df5/zstandard-0.25.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0be7622c37c183406f3dbf0cba104118eb16a4ea7359eeb5752f0794882fc250", size = 5433952, upload-time = "2025-09-14T22:17:45.271Z" }, + { url = "https://files.pythonhosted.org/packages/7b/01/b5f4d4dbc59ef193e870495c6f1275f5b2928e01ff5a81fecb22a06e22fb/zstandard-0.25.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5f5e4c2a23ca271c218ac025bd7d635597048b366d6f31f420aaeb715239fc98", size = 5814054, upload-time = "2025-09-14T22:17:47.08Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/fbd822d5c6f427cf158316d012c5a12f233473c2f9c5fe5ab1ae5d21f3d8/zstandard-0.25.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f187a0bb61b35119d1926aee039524d1f93aaf38a9916b8c4b78ac8514a0aaf", size = 5360113, upload-time = "2025-09-14T22:17:48.893Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/69a553d2047f9a2c7347caa225bb3a63b6d7704ad74610cb7823baa08ed7/zstandard-0.25.0-cp313-cp313-win32.whl", hash = "sha256:7030defa83eef3e51ff26f0b7bfb229f0204b66fe18e04359ce3474ac33cbc09", size = 436936, upload-time = "2025-09-14T22:17:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/b9c06c870f3bd8767c201f1edbdf9e8dc34be5b0fbc5682c4f80fe948475/zstandard-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:1f830a0dac88719af0ae43b8b2d6aef487d437036468ef3c2ea59c51f9d55fd5", size = 506232, upload-time = "2025-09-14T22:17:50.402Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/60c3c01243bb81d381c9916e2a6d9e149ab8627c0c7d7abb2d73384b3c0c/zstandard-0.25.0-cp313-cp313-win_arm64.whl", hash = "sha256:85304a43f4d513f5464ceb938aa02c1e78c2943b29f44a750b48b25ac999a049", size = 462671, upload-time = "2025-09-14T22:17:51.533Z" }, + { url = "https://files.pythonhosted.org/packages/3d/5c/f8923b595b55fe49e30612987ad8bf053aef555c14f05bb659dd5dbe3e8a/zstandard-0.25.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e29f0cf06974c899b2c188ef7f783607dbef36da4c242eb6c82dcd8b512855e3", size = 795887, upload-time = "2025-09-14T22:17:54.198Z" }, + { url = "https://files.pythonhosted.org/packages/8d/09/d0a2a14fc3439c5f874042dca72a79c70a532090b7ba0003be73fee37ae2/zstandard-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:05df5136bc5a011f33cd25bc9f506e7426c0c9b3f9954f056831ce68f3b6689f", size = 640658, upload-time = "2025-09-14T22:17:55.423Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/8b6b71b1ddd517f68ffb55e10834388d4f793c49c6b83effaaa05785b0b4/zstandard-0.25.0-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:f604efd28f239cc21b3adb53eb061e2a205dc164be408e553b41ba2ffe0ca15c", size = 5379849, upload-time = "2025-09-14T22:17:57.372Z" }, + { url = "https://files.pythonhosted.org/packages/a4/86/a48e56320d0a17189ab7a42645387334fba2200e904ee47fc5a26c1fd8ca/zstandard-0.25.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223415140608d0f0da010499eaa8ccdb9af210a543fac54bce15babbcfc78439", size = 5058095, upload-time = "2025-09-14T22:17:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ad/eb659984ee2c0a779f9d06dbfe45e2dc39d99ff40a319895df2d3d9a48e5/zstandard-0.25.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e54296a283f3ab5a26fc9b8b5d4978ea0532f37b231644f367aa588930aa043", size = 5551751, upload-time = "2025-09-14T22:18:01.618Z" }, + { url = "https://files.pythonhosted.org/packages/61/b3/b637faea43677eb7bd42ab204dfb7053bd5c4582bfe6b1baefa80ac0c47b/zstandard-0.25.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ca54090275939dc8ec5dea2d2afb400e0f83444b2fc24e07df7fdef677110859", size = 6364818, upload-time = "2025-09-14T22:18:03.769Z" }, + { url = "https://files.pythonhosted.org/packages/31/dc/cc50210e11e465c975462439a492516a73300ab8caa8f5e0902544fd748b/zstandard-0.25.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e09bb6252b6476d8d56100e8147b803befa9a12cea144bbe629dd508800d1ad0", size = 5560402, upload-time = "2025-09-14T22:18:05.954Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ae/56523ae9c142f0c08efd5e868a6da613ae76614eca1305259c3bf6a0ed43/zstandard-0.25.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a9ec8c642d1ec73287ae3e726792dd86c96f5681eb8df274a757bf62b750eae7", size = 4955108, upload-time = "2025-09-14T22:18:07.68Z" }, + { url = "https://files.pythonhosted.org/packages/98/cf/c899f2d6df0840d5e384cf4c4121458c72802e8bda19691f3b16619f51e9/zstandard-0.25.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a4089a10e598eae6393756b036e0f419e8c1d60f44a831520f9af41c14216cf2", size = 5269248, upload-time = "2025-09-14T22:18:09.753Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c0/59e912a531d91e1c192d3085fc0f6fb2852753c301a812d856d857ea03c6/zstandard-0.25.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f67e8f1a324a900e75b5e28ffb152bcac9fbed1cc7b43f99cd90f395c4375344", size = 5430330, upload-time = "2025-09-14T22:18:11.966Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/7e31db1240de2df22a58e2ea9a93fc6e38cc29353e660c0272b6735d6669/zstandard-0.25.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9654dbc012d8b06fc3d19cc825af3f7bf8ae242226df5f83936cb39f5fdc846c", size = 5811123, upload-time = "2025-09-14T22:18:13.907Z" }, + { url = "https://files.pythonhosted.org/packages/f6/49/fac46df5ad353d50535e118d6983069df68ca5908d4d65b8c466150a4ff1/zstandard-0.25.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4203ce3b31aec23012d3a4cf4a2ed64d12fea5269c49aed5e4c3611b938e4088", size = 5359591, upload-time = "2025-09-14T22:18:16.465Z" }, + { url = "https://files.pythonhosted.org/packages/c2/38/f249a2050ad1eea0bb364046153942e34abba95dd5520af199aed86fbb49/zstandard-0.25.0-cp314-cp314-win32.whl", hash = "sha256:da469dc041701583e34de852d8634703550348d5822e66a0c827d39b05365b12", size = 444513, upload-time = "2025-09-14T22:18:20.61Z" }, + { url = "https://files.pythonhosted.org/packages/3a/43/241f9615bcf8ba8903b3f0432da069e857fc4fd1783bd26183db53c4804b/zstandard-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:c19bcdd826e95671065f8692b5a4aa95c52dc7a02a4c5a0cac46deb879a017a2", size = 516118, upload-time = "2025-09-14T22:18:17.849Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/da163ce2450ed4febf6467d77ccb4cd52c4c30ab45624bad26ca0a27260c/zstandard-0.25.0-cp314-cp314-win_arm64.whl", hash = "sha256:d7541afd73985c630bafcd6338d2518ae96060075f9463d7dc14cfb33514383d", size = 476940, upload-time = "2025-09-14T22:18:19.088Z" }, +]