From ed557c3f1a24ac05d6d1772c61eb4f2d939bb071 Mon Sep 17 00:00:00 2001 From: "James N." Date: Mon, 12 Jan 2026 14:06:47 -0800 Subject: [PATCH 1/9] add ppt --- infra/ppt.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 infra/ppt.md diff --git a/infra/ppt.md b/infra/ppt.md new file mode 100644 index 000000000..9cdc0eeb9 --- /dev/null +++ b/infra/ppt.md @@ -0,0 +1,73 @@ +# Enterprise-Ready Agentic AI Architecture + +**From prototype to production: a secure, end-to-end blueprint for agentic AI on Azure** + +--- + +## What We Added + +| Feature | Description | +|---------|-------------| +| ✅ End-to-end agentic AI reference architecture | Complete stack from MCP tools → Agent orchestration → Backend → Frontend | +| ✅ Enterprise security by default | VNet integration, private endpoints, zero-trust managed identity | +| ✅ No secrets, no public exposure | Internal MCP, RBAC everywhere, HTTPS ingress only | +| ✅ Production-ready automation | Terraform/Bicep IaC + GitHub Actions CI/CD with OIDC | + +## Why It Matters + +| Gap | Solution | +|-----|----------| +| ❗ Industry lacks clear guidance for enterprise-grade agentic AI | ✅ Repeatable, opinionated blueprint from Dev → Prod | + +--- + +## Architecture Diagram + +```mermaid +flowchart LR + + User["👤 Users"] + + subgraph VNET["🛡️ Enterprise VNet"] + direction LR + + subgraph AGENTS["🤖 Agentic Layer"] + FE["🌐 Frontend"] + BE["⚙️ Agent Orchestrator"] + MCP["🔧 MCP Tools"] + end + + subgraph DATA["☁️ Azure Services"] + AOAI["🧠 OpenAI"] + COSMOS["💾 Cosmos DB"] + end + + subgraph SEC["🔐 Zero Trust"] + MI["🎫 Managed Identity"] + CICD["🚀 CI/CD"] + end + end + + User -->|HTTPS| FE + FE --> BE + BE --> MCP + BE --> AOAI + MCP --> COSMOS + + MI -.-> BE + MI -.-> MCP + CICD -.-> AGENTS + + %% Styling + classDef user fill:#1976D2,stroke:#0D47A1,stroke-width:3px,color:#fff + classDef frontend fill:#43A047,stroke:#1B5E20,stroke-width:2px,color:#fff + classDef agents fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 + classDef data fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff + classDef security fill:#00ACC1,stroke:#006064,stroke-width:2px,color:#fff + + class User user + class FE frontend + class BE,MCP agents + class AOAI,COSMOS data + class MI,CICD security +``` \ No newline at end of file From acdc7aa21fb96eef6a909ec96b8b89935e57db8e Mon Sep 17 00:00:00 2001 From: "James N." Date: Mon, 12 Jan 2026 14:18:46 -0800 Subject: [PATCH 2/9] add ppt --- infra/ppt.md | 110 +++++++++++++++++++++++++------------- infra/workshop_summary.md | 78 +++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 38 deletions(-) create mode 100644 infra/workshop_summary.md diff --git a/infra/ppt.md b/infra/ppt.md index 9cdc0eeb9..9163de1f1 100644 --- a/infra/ppt.md +++ b/infra/ppt.md @@ -24,50 +24,84 @@ ## Architecture Diagram ```mermaid -flowchart LR - - User["👤 Users"] - - subgraph VNET["🛡️ Enterprise VNet"] - direction LR - - subgraph AGENTS["🤖 Agentic Layer"] - FE["🌐 Frontend"] - BE["⚙️ Agent Orchestrator"] - MCP["🔧 MCP Tools"] +flowchart TB + + %% User / Client Layer + User["👤 Users & Apps
Web / Enterprise Clients"] + User -->|"🔒 HTTPS"| FE["🌐 Public Entry
Azure Container Apps
Managed TLS"] + + %% Enterprise VNet Boundary + subgraph VNET["🛡️ Enterprise VNet - Network Isolated"] + direction TB + + %% Agentic AI Layer + subgraph AGENTS["🤖 Agentic AI Layer"] + BE["⚙️ Agent Orchestrator
Backend Agent
Managed Identity"] + MCP["🔧 MCP Service
Internal Only
No Public Ingress"] + BE -->|"Internal HTTP"| MCP end - - subgraph DATA["☁️ Azure Services"] - AOAI["🧠 OpenAI"] - COSMOS["💾 Cosmos DB"] + + %% Platform & Data Layer + subgraph PLATFORM["☁️ Platform & Data Layer"] + AOAI["🧠 Azure OpenAI
Private Endpoint
RBAC Access"] + COSMOS["💾 Cosmos DB
Private Endpoint
RBAC Data Plane"] + ACR["📦 Container Registry
AcrPull via Identity"] end - - subgraph SEC["🔐 Zero Trust"] - MI["🎫 Managed Identity"] - CICD["🚀 CI/CD"] + + %% Security & Ops + subgraph SECURITY["🔐 Security & Operations"] + MI["🎫 Managed Identity
No API Keys"] + RBAC["👥 Azure RBAC
Least Privilege"] + CICD["🚀 GitHub Actions
OIDC Auth"] end + + %% Connections + FE --> BE + BE --> AOAI + MCP --> COSMOS + + BE -.->|"auth"| MI + MCP -.->|"auth"| MI + MI -.-> AOAI + MI -.-> COSMOS + + ACR -.->|"pull"| BE + ACR -.->|"pull"| MCP + end + + %% Environments + subgraph ENV["📊 Security Profiles"] + DEV["🟢 Dev
Minimal Security"] + STAGE["🟡 Staging
VNet + Internal MCP"] + PROD["🔴 Prod
Full Zero Trust"] end - User -->|HTTPS| FE - FE --> BE - BE --> MCP - BE --> AOAI - MCP --> COSMOS - - MI -.-> BE - MI -.-> MCP - CICD -.-> AGENTS - - %% Styling - classDef user fill:#1976D2,stroke:#0D47A1,stroke-width:3px,color:#fff - classDef frontend fill:#43A047,stroke:#1B5E20,stroke-width:2px,color:#fff - classDef agents fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 - classDef data fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff - classDef security fill:#00ACC1,stroke:#006064,stroke-width:2px,color:#fff + CICD --> DEV + CICD --> STAGE + CICD --> PROD + + %% Guidance Gap + GAP["⚠️ Industry Gap
Most samples stop at PoC
No VNet • API Keys
Public AI & DB"] + User -.->|"❌ Don't do this"| GAP + + %% Styling - Vibrant colors + classDef user fill:#1976D2,stroke:#0D47A1,stroke-width:3px,color:#fff,font-weight:bold + classDef entry fill:#43A047,stroke:#1B5E20,stroke-width:3px,color:#fff,font-weight:bold + classDef agents fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000,font-weight:bold + classDef platform fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff,font-weight:bold + classDef security fill:#00ACC1,stroke:#006064,stroke-width:2px,color:#fff,font-weight:bold + classDef envDev fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff + classDef envStage fill:#FFC107,stroke:#FF8F00,stroke-width:2px,color:#000 + classDef envProd fill:#F44336,stroke:#B71C1C,stroke-width:2px,color:#fff + classDef gap fill:#FFCDD2,stroke:#D32F2F,stroke-width:3px,stroke-dasharray:5 5,color:#B71C1C,font-weight:bold class User user - class FE frontend + class FE entry class BE,MCP agents - class AOAI,COSMOS data - class MI,CICD security + class AOAI,COSMOS,ACR platform + class MI,RBAC,CICD security + class DEV envDev + class STAGE envStage + class PROD envProd + class GAP gap ``` \ No newline at end of file diff --git a/infra/workshop_summary.md b/infra/workshop_summary.md new file mode 100644 index 000000000..8383c1cbb --- /dev/null +++ b/infra/workshop_summary.md @@ -0,0 +1,78 @@ +# Enterprise-Ready Agentic AI Infrastructure Workshop + +**Build and deploy secure, end-to-end agentic AI solutions on Azure** + +--- + +## Who Is This For + +Infrastructure engineers and enterprise architects with in-depth Azure knowledge who need to deploy agentic AI in an enterprise-grade manner. + +--- + +## What You'll Learn + +- ✅ **End-to-end agentic architecture** — MCP tools → Agent orchestration → Backend → Frontend +- ✅ **Your choice of IaC** — Bicep or Terraform, manual scripts or GitHub Actions +- ✅ **Modern identity principles** — OIDC for GitHub Actions, Managed Identity for Azure services (no keys) +- ✅ **Network isolation** — VNet with private endpoints, only frontend exposed to internet +- ✅ **Enterprise-ready template** — Scalable, reusable blueprint for standalone or landing zone deployment + +--- + +## Why It Matters + +Most agentic AI samples stop at proof-of-concept — public endpoints, API keys, no network isolation. This workshop provides a **repeatable, production-ready blueprint** from Dev → Prod. + +--- + +## Architecture Diagram + +```mermaid +flowchart LR + + User["👤 Users"] + + subgraph VNET["🛡️ Enterprise VNet"] + direction LR + + subgraph AGENTS["🤖 Agentic Layer"] + FE["🌐 Frontend"] + BE["⚙️ Agent Orchestrator"] + MCP["🔧 MCP Tools"] + end + + subgraph DATA["☁️ Azure Services"] + AOAI["🧠 OpenAI"] + COSMOS["💾 Cosmos DB"] + end + + subgraph SEC["🔐 Zero Trust"] + MI["🎫 Managed Identity"] + CICD["🚀 CI/CD"] + end + end + + User -->|HTTPS| FE + FE --> BE + BE --> MCP + BE --> AOAI + MCP --> COSMOS + + MI -.-> BE + MI -.-> MCP + CICD -.-> AGENTS + + %% Styling + classDef user fill:#1976D2,stroke:#0D47A1,stroke-width:3px,color:#fff + classDef frontend fill:#43A047,stroke:#1B5E20,stroke-width:2px,color:#fff + classDef agents fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 + classDef data fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff + classDef security fill:#00ACC1,stroke:#006064,stroke-width:2px,color:#fff + + class User user + class FE frontend + class BE,MCP agents + class AOAI,COSMOS data + class MI,CICD security +``` \ No newline at end of file From 2910b598358d6375ad1bbe80e0124e2e4752f503 Mon Sep 17 00:00:00 2001 From: "James N." Date: Mon, 12 Jan 2026 14:19:49 -0800 Subject: [PATCH 3/9] add ppt --- infra/ppt.md | 107 ------------------------------------ infra/workshop_summary.md | 110 +++++++++++++++++++++++++------------- 2 files changed, 72 insertions(+), 145 deletions(-) delete mode 100644 infra/ppt.md diff --git a/infra/ppt.md b/infra/ppt.md deleted file mode 100644 index 9163de1f1..000000000 --- a/infra/ppt.md +++ /dev/null @@ -1,107 +0,0 @@ -# Enterprise-Ready Agentic AI Architecture - -**From prototype to production: a secure, end-to-end blueprint for agentic AI on Azure** - ---- - -## What We Added - -| Feature | Description | -|---------|-------------| -| ✅ End-to-end agentic AI reference architecture | Complete stack from MCP tools → Agent orchestration → Backend → Frontend | -| ✅ Enterprise security by default | VNet integration, private endpoints, zero-trust managed identity | -| ✅ No secrets, no public exposure | Internal MCP, RBAC everywhere, HTTPS ingress only | -| ✅ Production-ready automation | Terraform/Bicep IaC + GitHub Actions CI/CD with OIDC | - -## Why It Matters - -| Gap | Solution | -|-----|----------| -| ❗ Industry lacks clear guidance for enterprise-grade agentic AI | ✅ Repeatable, opinionated blueprint from Dev → Prod | - ---- - -## Architecture Diagram - -```mermaid -flowchart TB - - %% User / Client Layer - User["👤 Users & Apps
Web / Enterprise Clients"] - User -->|"🔒 HTTPS"| FE["🌐 Public Entry
Azure Container Apps
Managed TLS"] - - %% Enterprise VNet Boundary - subgraph VNET["🛡️ Enterprise VNet - Network Isolated"] - direction TB - - %% Agentic AI Layer - subgraph AGENTS["🤖 Agentic AI Layer"] - BE["⚙️ Agent Orchestrator
Backend Agent
Managed Identity"] - MCP["🔧 MCP Service
Internal Only
No Public Ingress"] - BE -->|"Internal HTTP"| MCP - end - - %% Platform & Data Layer - subgraph PLATFORM["☁️ Platform & Data Layer"] - AOAI["🧠 Azure OpenAI
Private Endpoint
RBAC Access"] - COSMOS["💾 Cosmos DB
Private Endpoint
RBAC Data Plane"] - ACR["📦 Container Registry
AcrPull via Identity"] - end - - %% Security & Ops - subgraph SECURITY["🔐 Security & Operations"] - MI["🎫 Managed Identity
No API Keys"] - RBAC["👥 Azure RBAC
Least Privilege"] - CICD["🚀 GitHub Actions
OIDC Auth"] - end - - %% Connections - FE --> BE - BE --> AOAI - MCP --> COSMOS - - BE -.->|"auth"| MI - MCP -.->|"auth"| MI - MI -.-> AOAI - MI -.-> COSMOS - - ACR -.->|"pull"| BE - ACR -.->|"pull"| MCP - end - - %% Environments - subgraph ENV["📊 Security Profiles"] - DEV["🟢 Dev
Minimal Security"] - STAGE["🟡 Staging
VNet + Internal MCP"] - PROD["🔴 Prod
Full Zero Trust"] - end - - CICD --> DEV - CICD --> STAGE - CICD --> PROD - - %% Guidance Gap - GAP["⚠️ Industry Gap
Most samples stop at PoC
No VNet • API Keys
Public AI & DB"] - User -.->|"❌ Don't do this"| GAP - - %% Styling - Vibrant colors - classDef user fill:#1976D2,stroke:#0D47A1,stroke-width:3px,color:#fff,font-weight:bold - classDef entry fill:#43A047,stroke:#1B5E20,stroke-width:3px,color:#fff,font-weight:bold - classDef agents fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000,font-weight:bold - classDef platform fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff,font-weight:bold - classDef security fill:#00ACC1,stroke:#006064,stroke-width:2px,color:#fff,font-weight:bold - classDef envDev fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff - classDef envStage fill:#FFC107,stroke:#FF8F00,stroke-width:2px,color:#000 - classDef envProd fill:#F44336,stroke:#B71C1C,stroke-width:2px,color:#fff - classDef gap fill:#FFCDD2,stroke:#D32F2F,stroke-width:3px,stroke-dasharray:5 5,color:#B71C1C,font-weight:bold - - class User user - class FE entry - class BE,MCP agents - class AOAI,COSMOS,ACR platform - class MI,RBAC,CICD security - class DEV envDev - class STAGE envStage - class PROD envProd - class GAP gap -``` \ No newline at end of file diff --git a/infra/workshop_summary.md b/infra/workshop_summary.md index 8383c1cbb..5157cfb0d 100644 --- a/infra/workshop_summary.md +++ b/infra/workshop_summary.md @@ -29,50 +29,84 @@ Most agentic AI samples stop at proof-of-concept — public endpoints, API keys, ## Architecture Diagram ```mermaid -flowchart LR - - User["👤 Users"] - - subgraph VNET["🛡️ Enterprise VNet"] - direction LR - - subgraph AGENTS["🤖 Agentic Layer"] - FE["🌐 Frontend"] - BE["⚙️ Agent Orchestrator"] - MCP["🔧 MCP Tools"] +flowchart TB + + %% User / Client Layer + User["👤 Users & Apps
Web / Enterprise Clients"] + User -->|"🔒 HTTPS"| FE["🌐 Public Entry
Azure Container Apps
Managed TLS"] + + %% Enterprise VNet Boundary + subgraph VNET["🛡️ Enterprise VNet - Network Isolated"] + direction TB + + %% Agentic AI Layer + subgraph AGENTS["🤖 Agentic AI Layer"] + BE["⚙️ Agent Orchestrator
Backend Agent
Managed Identity"] + MCP["🔧 MCP Service
Internal Only
No Public Ingress"] + BE -->|"Internal HTTP"| MCP end - - subgraph DATA["☁️ Azure Services"] - AOAI["🧠 OpenAI"] - COSMOS["💾 Cosmos DB"] + + %% Platform & Data Layer + subgraph PLATFORM["☁️ Platform & Data Layer"] + AOAI["🧠 Azure OpenAI
Private Endpoint
RBAC Access"] + COSMOS["💾 Cosmos DB
Private Endpoint
RBAC Data Plane"] + ACR["📦 Container Registry
AcrPull via Identity"] end - - subgraph SEC["🔐 Zero Trust"] - MI["🎫 Managed Identity"] - CICD["🚀 CI/CD"] + + %% Security & Ops + subgraph SECURITY["🔐 Security & Operations"] + MI["🎫 Managed Identity
No API Keys"] + RBAC["👥 Azure RBAC
Least Privilege"] + CICD["🚀 GitHub Actions
OIDC Auth"] end + + %% Connections + FE --> BE + BE --> AOAI + MCP --> COSMOS + + BE -.->|"auth"| MI + MCP -.->|"auth"| MI + MI -.-> AOAI + MI -.-> COSMOS + + ACR -.->|"pull"| BE + ACR -.->|"pull"| MCP + end + + %% Environments + subgraph ENV["📊 Security Profiles"] + DEV["🟢 Dev
Minimal Security"] + STAGE["🟡 Staging
VNet + Internal MCP"] + PROD["🔴 Prod
Full Zero Trust"] end - User -->|HTTPS| FE - FE --> BE - BE --> MCP - BE --> AOAI - MCP --> COSMOS - - MI -.-> BE - MI -.-> MCP - CICD -.-> AGENTS - - %% Styling - classDef user fill:#1976D2,stroke:#0D47A1,stroke-width:3px,color:#fff - classDef frontend fill:#43A047,stroke:#1B5E20,stroke-width:2px,color:#fff - classDef agents fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000 - classDef data fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff - classDef security fill:#00ACC1,stroke:#006064,stroke-width:2px,color:#fff + CICD --> DEV + CICD --> STAGE + CICD --> PROD + + %% Guidance Gap + GAP["⚠️ Industry Gap
Most samples stop at PoC
No VNet • API Keys
Public AI & DB"] + User -.->|"❌ Don't do this"| GAP + + %% Styling - Vibrant colors + classDef user fill:#1976D2,stroke:#0D47A1,stroke-width:3px,color:#fff,font-weight:bold + classDef entry fill:#43A047,stroke:#1B5E20,stroke-width:3px,color:#fff,font-weight:bold + classDef agents fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000,font-weight:bold + classDef platform fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff,font-weight:bold + classDef security fill:#00ACC1,stroke:#006064,stroke-width:2px,color:#fff,font-weight:bold + classDef envDev fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff + classDef envStage fill:#FFC107,stroke:#FF8F00,stroke-width:2px,color:#000 + classDef envProd fill:#F44336,stroke:#B71C1C,stroke-width:2px,color:#fff + classDef gap fill:#FFCDD2,stroke:#D32F2F,stroke-width:3px,stroke-dasharray:5 5,color:#B71C1C,font-weight:bold class User user - class FE frontend + class FE entry class BE,MCP agents - class AOAI,COSMOS data - class MI,CICD security + class AOAI,COSMOS,ACR platform + class MI,RBAC,CICD security + class DEV envDev + class STAGE envStage + class PROD envProd + class GAP gap ``` \ No newline at end of file From ca35d9edb3c2aff797eb67bfb574cd2df78dc600 Mon Sep 17 00:00:00 2001 From: "James N." Date: Mon, 12 Jan 2026 14:28:58 -0800 Subject: [PATCH 4/9] clean up old documentation references --- infra/workshop_summary.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/workshop_summary.md b/infra/workshop_summary.md index 5157cfb0d..dfc656653 100644 --- a/infra/workshop_summary.md +++ b/infra/workshop_summary.md @@ -1,4 +1,4 @@ -# Enterprise-Ready Agentic AI Infrastructure Workshop +# Enterprise-Ready Agentic AI Workshop **Build and deploy secure, end-to-end agentic AI solutions on Azure** From fa1c0c6e7e577fb8dca29fc305c9b9afb7386d09 Mon Sep 17 00:00:00 2001 From: "James N." Date: Mon, 12 Jan 2026 14:33:09 -0800 Subject: [PATCH 5/9] add bullet point --- infra/workshop_summary.md | 1 + 1 file changed, 1 insertion(+) diff --git a/infra/workshop_summary.md b/infra/workshop_summary.md index dfc656653..eb9ab0be6 100644 --- a/infra/workshop_summary.md +++ b/infra/workshop_summary.md @@ -16,6 +16,7 @@ Infrastructure engineers and enterprise architects with in-depth Azure knowledge - ✅ **Your choice of IaC** — Bicep or Terraform, manual scripts or GitHub Actions - ✅ **Modern identity principles** — OIDC for GitHub Actions, Managed Identity for Azure services (no keys) - ✅ **Network isolation** — VNet with private endpoints, only frontend exposed to internet +- ✅ **Automated CI/CD pipelines** — GitHub Actions, parallel container builds, integration testing, multi-environment deployment - ✅ **Enterprise-ready template** — Scalable, reusable blueprint for standalone or landing zone deployment --- From 9c62328b508b34065f3722b678bc42d12ee7dd9e Mon Sep 17 00:00:00 2001 From: "James N." Date: Mon, 12 Jan 2026 14:38:02 -0800 Subject: [PATCH 6/9] add database --- infra/ppt.md | 107 ++++++++++++++++++++++++++++++++++++++ infra/workshop_summary.md | 2 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 infra/ppt.md diff --git a/infra/ppt.md b/infra/ppt.md new file mode 100644 index 000000000..9163de1f1 --- /dev/null +++ b/infra/ppt.md @@ -0,0 +1,107 @@ +# Enterprise-Ready Agentic AI Architecture + +**From prototype to production: a secure, end-to-end blueprint for agentic AI on Azure** + +--- + +## What We Added + +| Feature | Description | +|---------|-------------| +| ✅ End-to-end agentic AI reference architecture | Complete stack from MCP tools → Agent orchestration → Backend → Frontend | +| ✅ Enterprise security by default | VNet integration, private endpoints, zero-trust managed identity | +| ✅ No secrets, no public exposure | Internal MCP, RBAC everywhere, HTTPS ingress only | +| ✅ Production-ready automation | Terraform/Bicep IaC + GitHub Actions CI/CD with OIDC | + +## Why It Matters + +| Gap | Solution | +|-----|----------| +| ❗ Industry lacks clear guidance for enterprise-grade agentic AI | ✅ Repeatable, opinionated blueprint from Dev → Prod | + +--- + +## Architecture Diagram + +```mermaid +flowchart TB + + %% User / Client Layer + User["👤 Users & Apps
Web / Enterprise Clients"] + User -->|"🔒 HTTPS"| FE["🌐 Public Entry
Azure Container Apps
Managed TLS"] + + %% Enterprise VNet Boundary + subgraph VNET["🛡️ Enterprise VNet - Network Isolated"] + direction TB + + %% Agentic AI Layer + subgraph AGENTS["🤖 Agentic AI Layer"] + BE["⚙️ Agent Orchestrator
Backend Agent
Managed Identity"] + MCP["🔧 MCP Service
Internal Only
No Public Ingress"] + BE -->|"Internal HTTP"| MCP + end + + %% Platform & Data Layer + subgraph PLATFORM["☁️ Platform & Data Layer"] + AOAI["🧠 Azure OpenAI
Private Endpoint
RBAC Access"] + COSMOS["💾 Cosmos DB
Private Endpoint
RBAC Data Plane"] + ACR["📦 Container Registry
AcrPull via Identity"] + end + + %% Security & Ops + subgraph SECURITY["🔐 Security & Operations"] + MI["🎫 Managed Identity
No API Keys"] + RBAC["👥 Azure RBAC
Least Privilege"] + CICD["🚀 GitHub Actions
OIDC Auth"] + end + + %% Connections + FE --> BE + BE --> AOAI + MCP --> COSMOS + + BE -.->|"auth"| MI + MCP -.->|"auth"| MI + MI -.-> AOAI + MI -.-> COSMOS + + ACR -.->|"pull"| BE + ACR -.->|"pull"| MCP + end + + %% Environments + subgraph ENV["📊 Security Profiles"] + DEV["🟢 Dev
Minimal Security"] + STAGE["🟡 Staging
VNet + Internal MCP"] + PROD["🔴 Prod
Full Zero Trust"] + end + + CICD --> DEV + CICD --> STAGE + CICD --> PROD + + %% Guidance Gap + GAP["⚠️ Industry Gap
Most samples stop at PoC
No VNet • API Keys
Public AI & DB"] + User -.->|"❌ Don't do this"| GAP + + %% Styling - Vibrant colors + classDef user fill:#1976D2,stroke:#0D47A1,stroke-width:3px,color:#fff,font-weight:bold + classDef entry fill:#43A047,stroke:#1B5E20,stroke-width:3px,color:#fff,font-weight:bold + classDef agents fill:#FF9800,stroke:#E65100,stroke-width:2px,color:#000,font-weight:bold + classDef platform fill:#9C27B0,stroke:#4A148C,stroke-width:2px,color:#fff,font-weight:bold + classDef security fill:#00ACC1,stroke:#006064,stroke-width:2px,color:#fff,font-weight:bold + classDef envDev fill:#4CAF50,stroke:#2E7D32,stroke-width:2px,color:#fff + classDef envStage fill:#FFC107,stroke:#FF8F00,stroke-width:2px,color:#000 + classDef envProd fill:#F44336,stroke:#B71C1C,stroke-width:2px,color:#fff + classDef gap fill:#FFCDD2,stroke:#D32F2F,stroke-width:3px,stroke-dasharray:5 5,color:#B71C1C,font-weight:bold + + class User user + class FE entry + class BE,MCP agents + class AOAI,COSMOS,ACR platform + class MI,RBAC,CICD security + class DEV envDev + class STAGE envStage + class PROD envProd + class GAP gap +``` \ No newline at end of file diff --git a/infra/workshop_summary.md b/infra/workshop_summary.md index eb9ab0be6..e503fe30a 100644 --- a/infra/workshop_summary.md +++ b/infra/workshop_summary.md @@ -12,7 +12,7 @@ Infrastructure engineers and enterprise architects with in-depth Azure knowledge ## What You'll Learn -- ✅ **End-to-end agentic architecture** — MCP tools → Agent orchestration → Backend → Frontend +- ✅ **End-to-end agentic architecture** — Database → MCP tools → Agent orchestration → Backend → Frontend - ✅ **Your choice of IaC** — Bicep or Terraform, manual scripts or GitHub Actions - ✅ **Modern identity principles** — OIDC for GitHub Actions, Managed Identity for Azure services (no keys) - ✅ **Network isolation** — VNet with private endpoints, only frontend exposed to internet From b966c4c9ec550e39f166557ea6aa303501d4a5aa Mon Sep 17 00:00:00 2001 From: "James N." Date: Wed, 14 Jan 2026 14:35:52 -0800 Subject: [PATCH 7/9] add evaluation --- tests/evaluation/.env.template | 56 + tests/evaluation/README.md | 300 ++ tests/evaluation/__init__.py | 2 + .../evaluation/agent_comparison_results.json | 1611 +++++++++ tests/evaluation/agent_evaluator.py | 658 ++++ tests/evaluation/agent_runner.py | 376 ++ tests/evaluation/all_agents_comparison.json | 1 + tests/evaluation/llm_judge_evaluator.py | 759 ++++ tests/evaluation/pyproject.toml | 42 + tests/evaluation/test_agent_comparison.py | 484 +++ tests/evaluation/test_data.jsonl | 10 + tests/evaluation/test_scenario_evaluation.py | 2020 +++++++++++ tests/evaluation/uv.lock | 3198 +++++++++++++++++ tests/pytest.ini | 5 + tests/requirements.txt | 6 +- tests/test_agent_evaluation.py | 512 +++ 16 files changed, 10039 insertions(+), 1 deletion(-) create mode 100644 tests/evaluation/.env.template create mode 100644 tests/evaluation/README.md create mode 100644 tests/evaluation/__init__.py create mode 100644 tests/evaluation/agent_comparison_results.json create mode 100644 tests/evaluation/agent_evaluator.py create mode 100644 tests/evaluation/agent_runner.py create mode 100644 tests/evaluation/all_agents_comparison.json create mode 100644 tests/evaluation/llm_judge_evaluator.py create mode 100644 tests/evaluation/pyproject.toml create mode 100644 tests/evaluation/test_agent_comparison.py create mode 100644 tests/evaluation/test_data.jsonl create mode 100644 tests/evaluation/test_scenario_evaluation.py create mode 100644 tests/evaluation/uv.lock create mode 100644 tests/test_agent_evaluation.py diff --git a/tests/evaluation/.env.template b/tests/evaluation/.env.template new file mode 100644 index 000000000..d5dcfc766 --- /dev/null +++ b/tests/evaluation/.env.template @@ -0,0 +1,56 @@ +# Agent Evaluation Environment Configuration +# Copy this file to .env and fill in your Azure OpenAI credentials + +# ═══════════════════════════════════════════════════════════════════════════════ +# AZURE OPENAI CONFIGURATION (Required) +# ═══════════════════════════════════════════════════════════════════════════════ +AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com" +AZURE_OPENAI_KEY="your-api-key-here" +AZURE_OPENAI_API_KEY="your-api-key-here" # Alias for compatibility +AZURE_OPENAI_CHAT_DEPLOYMENT="gpt-4.1" +AZURE_OPENAI_DEPLOYMENT="gpt-4.1" # For LLM-as-judge evaluators +AZURE_OPENAI_API_VERSION="2025-03-01-preview" + +# ═══════════════════════════════════════════════════════════════════════════════ +# LLM-AS-JUDGE CONFIGURATION (For AI-assisted evaluation) +# ═══════════════════════════════════════════════════════════════════════════════ +# Model deployment for LLM judge evaluators (recommend gpt-4o or better) +# Set USE_REASONING_MODEL=true if using o-series models (o1, o3-mini) +LLM_JUDGE_DEPLOYMENT="gpt-4o" +USE_REASONING_MODEL="false" + +# ═══════════════════════════════════════════════════════════════════════════════ +# MCP SERVER CONFIGURATION (Required for integration tests) +# ═══════════════════════════════════════════════════════════════════════════════ +MCP_SERVER_URI="http://localhost:8000/mcp" + +# ═══════════════════════════════════════════════════════════════════════════════ +# AGENT MODULES TO EVALUATE +# ═══════════════════════════════════════════════════════════════════════════════ +# Single Agent - Basic intelligent agent with MCP tools +SINGLE_AGENT_MODULE="agents.agent_framework.single_agent" + +# Reflection Agent - Primary + Reviewer pattern for quality assurance +REFLECTION_AGENT_MODULE="agents.agent_framework.multi_agent.reflection_agent" + +# Default agent for single-agent tests +DEFAULT_AGENT_MODULE="agents.agent_framework.single_agent" + +# ═══════════════════════════════════════════════════════════════════════════════ +# EVALUATION SETTINGS +# ═══════════════════════════════════════════════════════════════════════════════ +# Number of test cases to run in quick mode (default: 3) +EVAL_QUICK_TEST_COUNT=3 + +# Enable LLM-as-judge evaluation (uses additional Azure OpenAI calls) +# When true, uses Azure AI Foundry evaluators (IntentResolution, TaskAdherence, etc.) +# When false, uses simple keyword matching for outcome evaluation +EVAL_USE_LLM_JUDGE="true" + +# Tool call accuracy threshold (0.0 - 1.0) +EVAL_TOOL_ACCURACY_THRESHOLD=0.5 + +# ═══════════════════════════════════════════════════════════════════════════════ +# OPTIONAL: Embedding model for AI evaluation +# ═══════════════════════════════════════════════════════════════════════════════ +AZURE_OPENAI_EMBEDDING_DEPLOYMENT="text-embedding-ada-002" diff --git a/tests/evaluation/README.md b/tests/evaluation/README.md new file mode 100644 index 000000000..1ab1867fb --- /dev/null +++ b/tests/evaluation/README.md @@ -0,0 +1,300 @@ +# Agent Evaluation Framework + +This directory contains a comprehensive evaluation framework for AI agents using the **Azure AI Foundry Evaluation SDK** with **LLM-as-Judge** capabilities. + +## 📋 Overview + +The evaluation framework tests agent performance across multiple dimensions: + +### Evaluation Types + +| Type | Description | +|------|-------------| +| **Process-Based** | Evaluates HOW the agent works (tool calls, reasoning steps) | +| **Goal-Based** | Evaluates WHAT the agent achieves (outcome quality) | + +### LLM-as-Judge Evaluators (Azure AI Foundry) + +| Evaluator | Type | Description | +|-----------|------|-------------| +| `IntentResolutionEvaluator` | Goal | Did the agent correctly identify user intent? | +| `TaskAdherenceEvaluator` | Goal | Did the response follow the assigned task? | +| `ToolCallAccuracyEvaluator` | Process | Were the correct tools called? | +| `CoherenceEvaluator` | Quality | Is the response logically coherent? | +| `FluencyEvaluator` | Quality | Is the language natural? | +| `RelevanceEvaluator` | Quality | Is the response relevant? | + +### Fallback Metrics (No LLM Required) + +| Metric | Description | +|--------|-------------| +| **Tool Recall/Precision/F1** | Rule-based tool call accuracy | +| **Keyword Coverage** | Simple keyword matching for outcomes | + +## 🆕 Standalone Setup (Recommended) + +The evaluation module runs **independently** from the applications folder. + +### Prerequisites + +1. **MCP Server** running at `http://localhost:8000/mcp` +2. **Azure OpenAI** credentials (gpt-4.1 or gpt-4o recommended for LLM judges) +3. **uv** package manager + +### Quick Start + +```bash +# Navigate to evaluation folder +cd tests/evaluation + +# Install dependencies (first time only) +$env:UV_LINK_MODE="copy" # Windows only, for OneDrive compatibility +uv sync + +# Run quick comparison (3 test cases, ~1 min) +uv run pytest test_agent_comparison.py::TestAgentComparison::test_quick_comparison -v -s + +# Run full comparison with LLM judges +uv run pytest test_scenario_evaluation.py::TestAgentComparison -v -s + +# Test LLM judge directly +uv run python llm_judge_evaluator.py +``` + +### Configuration + +Edit `.env` file in `tests/evaluation/` to configure: + +```bash +# Azure OpenAI for agents +AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com +AZURE_OPENAI_API_KEY=your-api-key +AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-5.2-chat + +# LLM Judge (use gpt-4.1 for compatibility with azure-ai-evaluation SDK) +AZURE_OPENAI_DEPLOYMENT=gpt-4.1 +LLM_JUDGE_DEPLOYMENT=gpt-4.1 +USE_REASONING_MODEL=false # Set true for o-series models + +# MCP Server +MCP_SERVER_URI=http://localhost:8000/mcp + +# Evaluation settings +EVAL_USE_LLM_JUDGE=true +EVAL_QUICK_TEST_COUNT=3 +``` + +--- + +## 🧠 LLM-as-Judge Feature + +Instead of simple keyword matching, the framework uses Azure AI Foundry's LLM-based evaluators: + +```python +from llm_judge_evaluator import LLMJudgeEvaluator, ToolCall, ToolDefinition + +evaluator = LLMJudgeEvaluator() + +result = await evaluator.evaluate( + query="What is my invoice total?", + response="Your invoice total is $542.50...", + tool_calls=[ToolCall(name="get_customer_invoices", arguments={"customer_id": 123})], + tool_definitions=[ToolDefinition(name="get_customer_invoices", description="Get invoices")] +) + +print(f"Intent Resolution: {result.intent_resolution_score}/5 - {result.intent_resolution_result}") +print(f"Task Adherence: {result.task_adherence_score}/5 - {result.task_adherence_result}") +print(f"Tool Accuracy: {result.tool_call_accuracy_score}/5 - {result.tool_call_accuracy_result}") +``` + +### Multi-Turn Support + +Azure AI Foundry evaluators support full conversation history: + +```python +from llm_judge_evaluator import ConversationMessage + +conversation = [ + ConversationMessage(role="user", content="What's my account status?"), + ConversationMessage(role="assistant", content="Let me check...", tool_calls=[...]), + ConversationMessage(role="tool", content='{"status": "active"}', tool_call_id="call_1"), + ConversationMessage(role="assistant", content="Your account is active."), +] + +result = await evaluator.evaluate( + query="What's my account status?", + response="Your account is active.", + conversation=conversation, + system_prompt="You are a helpful customer service agent." +) +``` + +--- + +## 🔄 Agent Comparison + +The framework compares **single_agent** vs **reflection_agent**: + +| Agent | Description | Expected Performance | +|-------|-------------|---------------------| +| **single_agent** | Direct LLM response | Faster, baseline quality | +| **reflection_agent** | Primary + Reviewer pattern | Slower, higher quality | + +### Sample Output + +``` +══════════════════════════════════════════════════════════════════════ +AGENT COMPARISON REPORT: single_agent vs reflection_agent +══════════════════════════════════════════════════════════════════════ +Test Cases: 3 + +Metric single_agent reflection_agent Diff +---------------------------------------------------------------------- +avg_execution_time 6.043 12.338 +6.295 +avg_response_length 936.667 1127.667 +191.000 +success_rate 1.000 1.000 0.000 +---------------------------------------------------------------------- +``` + +--- + +## 🚀 Legacy Quick Start (from applications folder) + +### Run Unit Tests (No external dependencies) + +```bash +# From the applications folder (recommended for uv) +cd agentic_ai/applications +uv run python -m pytest ../../tests/test_agent_evaluation.py -v -m "unit" +``` + +### Run Integration Tests (Requires MCP server + Azure OpenAI) + +1. **Start MCP server:** +```bash +cd mcp && uv run python mcp_service.py +``` + +2. **Start Backend:** +```bash +cd agentic_ai/applications && uv run python backend.py +``` + +3. **Run evaluation tests:** +```bash +cd agentic_ai/applications +uv run python -m pytest ../../tests/test_agent_evaluation.py -v -m "evaluation and integration" +``` + +### Run Full Evaluation Pipeline + +```bash +cd agentic_ai/applications + +# Run with AI-assisted evaluation +uv run python -m tests.evaluation.agent_evaluator \ + --test-data ../../tests/evaluation/test_data.jsonl \ + --agent-module agents.agent_framework.single_agent \ + --output ../../tests/evaluation/results.json + +# Run without AI evaluation (faster, no extra API costs) +uv run python -m tests.evaluation.agent_evaluator \ + --test-data ../../tests/evaluation/test_data.jsonl \ + --no-ai-eval +``` + +## 📁 Files + +| File | Description | +|------|-------------| +| `llm_judge_evaluator.py` | **NEW** LLM-as-Judge evaluator using Azure AI Foundry SDK | +| `agent_runner.py` | Generic agent test runner that works with any agent | +| `test_scenario_evaluation.py` | Scenario-based evaluation with dual metrics | +| `test_agent_comparison.py` | Agent comparison tests (single vs reflection) | +| `agent_evaluator.py` | Core evaluation module with AgentRunner and AgentEvaluator | +| `test_data.jsonl` | Test dataset with Contoso Communications scenarios | +| `pyproject.toml` | Standalone dependencies | +| `.env` | Environment configuration | + +## 📊 Test Data Format + +Test cases are stored in JSONL format: + +```json +{ + "query": "What's my billing summary?", + "customer_id": "251", + "expected_intent": "billing_inquiry", + "expected_tools": ["get_billing_summary", "get_customer_detail"], + "ground_truth": "The agent should retrieve and present the customer's billing summary.", + "category": "billing", + "complexity": "low" +} +``` + +### Categories Covered + +- **billing** - Invoice, payment, and balance queries +- **technical_support** - Service issues, data usage, connectivity +- **products** - Plan upgrades, international roaming +- **security** - Account lockout, authentication issues +- **promotions** - Discounts, loyalty rewards +- **support** - Support tickets, order returns + +## 🎯 Evaluation Thresholds + +Default thresholds (configurable in `EvaluationThresholds`): + +| Metric | Threshold | Description | +|--------|-----------|-------------| +| Tool Call Accuracy | 0.5 | F1 score for tool calls (lower to account for agent using tool subsets) | +| Groundedness | 0.7 | Normalized score (1-5 scale) | +| Relevance | 0.8 | Normalized score (1-5 scale) | +| Coherence | 0.8 | Normalized score (1-5 scale) | +| Fluency | 0.8 | Normalized score (1-5 scale) | + +## 🔧 CI/CD Integration + +The evaluation runs automatically in CI/CD: + +1. **On PR** (changes to `agentic_ai/agents/**`): Unit tests only +2. **On workflow_call**: Full integration tests against deployed services +3. **Manual trigger**: Optional full evaluation with AI metrics + +### GitHub Actions Workflow + +```yaml +# Trigger evaluation manually +gh workflow run agent-evaluation.yml \ + -f environment=dev \ + -f run_full_evaluation=true \ + -f include_ai_evaluation=true +``` + +## 📈 Sample Output + +``` +============================================================ +EVALUATION SUMMARY +============================================================ +Total Tests: 10 +Passed: 8 +Failed: 2 +Pass Rate: 80.0% +Avg Tool F1: 0.85 +Avg Exec Time: 1523ms + +By Category: + billing: 3/3 (100%) + technical_support: 2/2 (100%) + products: 1/2 (50%) + security: 1/1 (100%) + promotions: 1/2 (50%) +============================================================ +``` + +## 🔗 Related Documentation + +- [Azure AI Evaluation SDK](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/develop/agent-evaluate-sdk) +- [Contoso Communications Scenario](../../SCENARIO.md) +- [Microsoft Agent Framework](../agentic_ai/agents/agent_framework/README.md) diff --git a/tests/evaluation/__init__.py b/tests/evaluation/__init__.py new file mode 100644 index 000000000..9c57ca8fd --- /dev/null +++ b/tests/evaluation/__init__.py @@ -0,0 +1,2 @@ +# Agent Evaluation Module +# Comprehensive evaluation framework for AI Agents using Azure AI Evaluation SDK diff --git a/tests/evaluation/agent_comparison_results.json b/tests/evaluation/agent_comparison_results.json new file mode 100644 index 000000000..0617c2d92 --- /dev/null +++ b/tests/evaluation/agent_comparison_results.json @@ -0,0 +1,1611 @@ +{ + "single": [ + { + "scenario": "billing_high_invoice", + "scenario_name": "Invoice Higher Than Usual", + "success": true, + "tool_recall": 0.4, + "tool_precision": 1.0, + "tool_f1": 0.5714285714285715, + "keyword_coverage": 0.8333333333333334, + "total_time": 10.795327425003052, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to understand why their last invoice was unusually high. The agent provided a clear breakdown, explained the overage charges, and offered next steps. The response is thorough, accurate, and directly resolves the user's intent.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The assistant provided a detailed breakdown of the invoice and explained why the charge was higher than usual, offering relevant next steps such as reviewing usage or upgrading the plan. However, the assistant referenced specific invoice details (date, overage charges, plan type, payments made) without any corroborating evidence from tool outputs. Since the TOOL_CALLS are empty and such details appear to be fabricated rather than verified, this constitutes a material failure in verification and alignment with allowed workflows. No safety or privacy rules were breached, but claiming external, user-specific data without verified access is a procedural failure.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response correctly identifies that the higher invoice is due to data overage charges and mentions the 10GB data cap, which addresses the root cause. However, it does not specify the exact amount of data used (22GB), the precise overage (12GB), or the per-GB overage rate ($7.50/GB), which are key details per the ground truth. The explanation is somewhat vague about the calculation of the extra charges and does not break down the $90 overage fee. While the agent offers to help upgrade the plan and review usage, it does not explicitly offer a one-time courtesy adjustment or set up data alerts, which are recommended solutions. Overall, the response is adequate but lacks the specific numbers and full solution options required for a higher score.", + "llm_eval_time": 34.66149950027466 + }, + { + "scenario": "billing_payment_history", + "scenario_name": "Payment History Inquiry", + "success": true, + "tool_recall": 0.5, + "tool_precision": 0.5, + "tool_f1": 0.5, + "keyword_coverage": 0.6, + "total_time": 10.674136638641357, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to see their recent payments and confirm if they all went through. The agent provided a detailed, accurate summary of each payment and its status, clearly indicating which were successful and which had issues, fully resolving the intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant presented a clear and detailed summary of recent payments for customer 5, explicitly indicating which payments went through successfully and which were incomplete or had issues. The information matches the user's request to review recent payments for confirmation. There are no indications of unauthorized actions, privacy breaches, or unsubstantiated claims since no external tool output was required or referenced, and the content is appropriate and relevant. The outcome is complete and fulfills the user's intent in a usable format without any material failures.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response fully meets the criteria for an excellent answer as outlined in the rubric. It provides a detailed payment history, listing each recent payment with invoice numbers, amounts, payment methods, and the status of each (successful, partial, or failed). The response clearly identifies any failed or pending payments and specifies the remaining balances for those invoices. It also confirms which payments were successful and offers helpful next steps, such as assistance with paying remaining balances, checking the total amount due, and setting up autopay. The agent's message is clear, accurate, and directly addresses the customer's needs, matching all key points in the ground truth solution.", + "llm_eval_time": 35.0008647441864 + }, + { + "scenario": "billing_autopay_setup", + "scenario_name": "Autopay Setup Request", + "success": true, + "tool_recall": 0.25, + "tool_precision": 0.5, + "tool_f1": 0.3333333333333333, + "keyword_coverage": 0.4, + "total_time": 19.29434609413147, + "tools_called": [ + "get_customer_detail", + "update_subscription", + "update_subscription", + "update_subscription" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted help setting up autopay for their bill. The agent attempted to enable autopay, explained the failure, and offered actionable next steps, including opening a support ticket. While autopay wasn't enabled directly, the agent provided a clear path to resolution.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant understood the user's request to set up autopay for their bill. It attempted to enable autopay but encountered a system validation error, which was properly disclosed. The assistant did not fabricate a successful result and provided alternative solutions, including opening a support ticket or checking for blocking issues, which are reasonable next steps given its current limits. There are no signs of privacy breaches, improper tool use, unauthorized actions, or violations of presentation or workflow. The response is relevant and maintains focus on the user's objective without making verifiable claims outside what tool interactions support.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response provides basic autopay information and attempts to check and enable autopay for the customer, which aligns with the \"Adequate\" level on the rubric. The agent identifies the root cause (a system validation error preventing autopay activation) and offers next steps, such as opening a support ticket or checking for outstanding balances. However, the response does not mention the $5 monthly autopay discount, does not explain how autopay works (e.g., auto-charge on due date), and does not confirm the payment method on file. While the agent does check the current status and offers to help further, it misses key details required for a higher score.", + "llm_eval_time": 23.657154321670532 + }, + { + "scenario": "billing_overdue_invoice", + "scenario_name": "Overdue Invoice Question", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 1.0, + "total_time": 9.471482276916504, + "tools_called": [ + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know the consequences of not paying an overdue invoice. The agent provided a clear, thorough explanation of progressive outcomes, including late fees, service suspension, delinquency, and collections, fully resolving the intent with relevant details and helpful options.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant provided a detailed and clear explanation of the typical consequences of not paying an overdue invoice, which matches the user's request for information. There is no indication of rule violations\u2014no sensitive information was exposed, no harmful content generated, and no unauthorized actions taken. The assistant did not attempt any real-world actions or claim external results that would require verification through tool interactions; its response was informational only. There is no evidence of workflow or sequencing errors, as all information was directly relevant to the user's inquiry. No material failures were identified across any axis.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response explains the consequences of overdue invoices in detail, including late fees, service suspension, delinquency, and possible collections, which aligns well with the ground truth's requirements for explaining consequences. It also offers solutions such as payment arrangements and invoice adjustments, and invites the customer to check specific overdue amounts and process payment, addressing the customer's needs and next steps. However, the response does not proactively list the specific overdue invoices, amounts, or due dates, which is a key element in the ground truth for a perfect score. The response is thorough and helpful but lacks the direct provision of account-specific details, making it \"Good\" but not \"Excellent\" per the rubric.", + "llm_eval_time": 25.370789289474487 + }, + { + "scenario": "billing_refund_request", + "scenario_name": "Refund Request for Service Issue", + "success": true, + "tool_recall": 0.5, + "tool_precision": 0.6666666666666666, + "tool_f1": 0.5714285714285715, + "keyword_coverage": 0.8, + "total_time": 11.742409467697144, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail", + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user requested a refund or credit for 3 days without internet. The agent explained eligibility, confirmed a service incident, and offered to submit a credit request, clearly outlining next steps. The response is thorough and actionable, fully resolving the intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant provided a clear and relevant response to the user's inquiry about a refund or credit for a three-day internet outage. It did not make unsupported claims, offered next steps for submitting a request, and did not take or claim real-world actions without evidence. No sensitive information was disclosed, and all content was appropriate and aligned with the user's intent. No required workflows or rule-based constraints were violated.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response acknowledges the service outage and offers an appropriate solution by proposing a pro-rated credit for the 3 days of disruption, which aligns with the ground truth. The agent verifies the outage by referencing a service incident on the customer's subscription and explains the need for a formal review per company policy. The response outlines next steps, including opening a billing/support ticket and clarifies when the credit would appear. However, it does not explicitly state that the credit will be applied or confirm its application\u2014approval is contingent on the review process, and the agent asks for the customer's confirmation to proceed. While thorough and customer-focused, the response falls slightly short of the \"Excellent\" rubric level, which requires direct application and confirmation of the credit.", + "llm_eval_time": 27.133212327957153 + }, + { + "scenario": "internet_slow", + "scenario_name": "Internet Slower Than Before", + "success": true, + "tool_recall": 0.25, + "tool_precision": 0.3333333333333333, + "tool_f1": 0.28571428571428575, + "keyword_coverage": 0.4, + "total_time": 12.916604042053223, + "tools_called": [ + "get_customer_detail", + "get_data_usage", + "create_support_ticket" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User reported slow internet despite paying for 1Gbps. Agent confirmed the issue, opened a high-priority support ticket, explained next steps, and offered further help. The response is thorough, proactive, and fully resolves the user's intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant correctly identified the user's issue (slow internet despite paying for 1Gbps), reviewed their service status and plan, and provided a clear summary of relevant account information. It took appropriate action by opening a high-priority support ticket and transparently shared the ticket details. Additionally, it provided reasonable next steps and troubleshooting suggestions, offering further assistance as needed. There are no fabricated claims of external actions, no privacy or safety violations, and no evidence of procedural errors. The absence of TOOL_CALLS aligns with the type of actions claimed, which are primarily internal support tasks. The output directly addresses the user's request and provides a usable, goal-oriented response.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response correctly identifies that the customer's service status is marked as \"slow,\" indicating a known performance issue, and acknowledges the frustration, which aligns with the ground truth's requirement to recognize the incident and apologize. The agent provides basic troubleshooting steps (restart router, test wired connection, check device usage) and proactively opens a high-priority support ticket, offering to monitor the ticket and assist further. However, the response does not mention the specific date the incident was reported (April 17) or explicitly state that there is an open service incident affecting the area, nor does it mention the possibility of a service credit once the issue is resolved. These omissions prevent it from achieving a perfect score, but overall, the response is thorough and customer-focused, meeting most of the rubric's criteria for a \"Good\" rating.", + "llm_eval_time": 23.770139694213867 + }, + { + "scenario": "internet_upgrade_inquiry", + "scenario_name": "Internet Speed Upgrade Options", + "success": true, + "tool_recall": 0.5, + "tool_precision": 1.0, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 0.6666666666666666, + "total_time": 11.915200471878052, + "tools_called": [ + "get_customer_detail", + "get_products" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted upgrade options for faster internet suitable for video calls. Agent provided a detailed review of the current plan, recommended higher speed and data tiers, and mentioned bundle options, fully addressing the intent with relevant, actionable choices and next steps.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant addressed the user's concern about slow internet for video calls, reviewed the current plan, and offered relevant upgrade options including higher speed, increased data cap, and a bundle. The assistant provided context for why upgrades would help and asked clarifying questions to tailor recommendations, aligning with the user's work-from-home scenario. There are no unauthorized actions, privacy issues, or procedural errors. The outcome is complete and usable for the user's goal.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response is good, as it shows the customer's current plan details (speed tier, data cap, status), presents upgrade options with relevant speed tiers (500 Mbps, 1 Gbps), and explains the benefits of upgrading in terms of speed and data cap. It also mentions the possibility of bundle plans and Gold loyalty promotions, which aligns with the rubric's recommendation to offer applicable promotions. The agent makes a recommendation based on the customer's work-from-home and video call needs, asking clarifying questions to further personalize the upgrade. However, the response does not provide specific pricing for the upgrade options, nor does it explicitly show the price difference from the current plan or mention benefits like a WiFi 6 router or priority support. These omissions prevent it from reaching the \"Excellent\" level (score 5), but it fulfills the requirements for a \"Good\" score (4) by presenting options, making a recommendation, and addressing the customer's needs.", + "llm_eval_time": 37.07470417022705 + }, + { + "scenario": "internet_router_reset", + "scenario_name": "Router Reset Help", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.8333333333333334, + "total_time": 10.020252227783203, + "tools_called": [ + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted clear instructions on how to reset their router. The agent provided detailed, step-by-step guidance for both soft and factory resets, included warnings, and offered further support options, fully resolving the intent with thoroughness and relevance.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant understood the user's problem (router not working) and provided step-by-step instructions for both a soft reset and a factory reset, clearly describing differences and consequences. There is no omission of essential information and no extraneous or distracting content. The instructions are accurate, safe, and do not expose sensitive information. No unauthorized or unverifiable external actions are claimed, and no tool usage is referenced beyond an offer to assist further. There are no procedural or presentation errors, and no violation of any explicit constraints. All evaluation dimensions are met.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response provides complete, step-by-step instructions for both a soft reset and a factory reset, clearly distinguishing between the two and warning that the factory reset will erase custom settings, which aligns with the factory settings warning in the rubric. The instructions include accurate details such as locating the reset button, using a paperclip, holding for 10\u201315 seconds, and waiting for the router to reboot, matching the ground truth solution. The agent also offers additional help by suggesting to check for outages, create a support ticket, or walk the customer through reconnecting devices, and offers to check the account or open a support ticket, which covers the rubric's requirement to offer further assistance. Overall, the response is thorough, accurate, and customer-focused, fully addressing the customer's needs.", + "llm_eval_time": 24.50334358215332 + }, + { + "scenario": "internet_outage_report", + "scenario_name": "Internet Outage Report", + "success": true, + "tool_recall": 0.5, + "tool_precision": 1.0, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 1.0, + "total_time": 11.861270427703857, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know if there was an internet outage in their area. The agent checked for outages, confirmed none were reported, explained possible causes, suggested troubleshooting, and offered to open a support ticket, thoroughly resolving the intent with clear next steps.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user asked about a possible internet outage in their area, and the assistant responded by checking and stating that there are no reported service incidents or outages for the user's area or service. Although TOOL_CALLS are empty (no actual tool verification of outage status), the assistant did not claim any real-world effects or unverifiable external data that contradicts what can be assumed from the input constraints\u2014it merely simulated an internal check, which is acceptable for responses when tool output isn't provided and no explicit tool requirement is stated. The assistant provided troubleshooting steps and offered to escalate with a support ticket, fully aligning with the user's goal and constraints given. There are no safety, privacy, or procedural failures apparent in the response.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response checks the customer's subscription status and confirms there are no reported outages, which aligns with the ground truth's first two key actions. It provides troubleshooting steps and offers to create a support ticket for further investigation, including a technician visit if needed. The response is empathetic and addresses the customer's needs by explaining possible causes and next steps. However, it does not mention offering a service credit for extended outages or proactively offering to notify the customer when service is restored, which are part of the ideal solution. Overall, the response is thorough and appropriate, but missing a couple of elements for a perfect score.", + "llm_eval_time": 34.99604535102844 + }, + { + "scenario": "internet_static_ip", + "scenario_name": "Static IP Request", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.6666666666666666, + "total_time": 9.186568260192871, + "tools_called": [ + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know if a static IP address is available for their home server. The agent clearly explained availability, requirements, and next steps, offering to check eligibility or initiate provisioning, fully resolving the intent with thorough and relevant information.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant addresses the user's request by confirming that static IP addresses are offered, provides relevant details about eligibility and requirements, and offers to help further by checking the user's subscription or initiating the process. The information is on topic and sufficient for the user's initial query, with next steps presented. There are no claims of completed external actions without supporting evidence and no privacy, safety, or procedural violations. The response is complete, relevant, and free of extraneous content.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response explains that static IPs are available, clarifies that they are typically offered with business or advanced residential plans, and notes that standard plans come with dynamic IPs. It also outlines the need to check the customer's current plan and offers to help with upgrades or provisioning a static IP. However, the response lacks specific details about which exact plans include static IPs and their pricing, as outlined in the ground truth solution. While the agent provides accurate general information and appropriate next steps, the absence of plan names and prices prevents it from being a perfect answer. Thus, it meets the \"Good\" criteria but falls short of \"Excellent.\"", + "llm_eval_time": 22.03885006904602 + }, + { + "scenario": "roaming_travel", + "scenario_name": "Travelling Abroad - Needs Roaming", + "success": true, + "tool_recall": 0.25, + "tool_precision": 0.5, + "tool_f1": 0.3333333333333333, + "keyword_coverage": 0.75, + "total_time": 11.262126922607422, + "tools_called": [ + "get_customer_detail", + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted information about international roaming for their upcoming trip to Spain. The agent provided detailed, relevant information about roaming status, options, timing, and next steps, fully addressing the user's needs and offering to enable roaming, thus excellently resolving the intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant provided a thorough overview of international roaming for the user's upcoming trip to Spain, clearly stating the user's current status, available options, and relevant timing concerns, and offered actionable next steps without making unverifiable claims or performing unauthorized actions. No sensitive data was exposed, no presentation rules were violated, and there were no tool actions incorrectly claimed. The response directly addressed the user's stated need for roaming information and supported follow-up requests, resulting in a usable and compliant output.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response fully meets the criteria for an excellent answer. It clearly identifies that international roaming is not enabled on the customer's account and acknowledges the urgency due to the 2-day timeline, referencing the recommended 3-day activation window. The agent confirms that Spain is covered under the roaming options and describes the available packages (voice, text, data, and extra data add-ons). The response offers to urgently enable roaming, recommend suitable packages, and set up usage alerts to prevent unexpected charges. All key facts and recommended actions from the ground truth are addressed, and the response is proactive, accurate, and customer-focused.", + "llm_eval_time": 24.91675329208374 + }, + { + "scenario": "mobile_data_usage", + "scenario_name": "Mobile Data Usage Check", + "success": true, + "tool_recall": 0.6666666666666666, + "tool_precision": 1.0, + "tool_f1": 0.8, + "keyword_coverage": 0.8, + "total_time": 9.09735631942749, + "tools_called": [ + "get_customer_detail", + "get_data_usage" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know their current data usage and ensure they are not close to their limit. Agent provided the exact usage (0 GB), the monthly cap (100 GB), and confirmed the user is well within their limit, fully resolving the intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant answered the user's query regarding their data usage and current limit. The user asked for their usage this month, and the assistant provided the data used (0 GB), the cap (100 GB), and confirmed the user is well within their limit. No extraneous or unrelated actions were taken, and no privacy, safety, or authorization rules were violated. There is no evidence of tool calls or external data being necessary or claimed erroneously. The result is fully usable and aligns with the user's intent.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response provides the customer's current data usage (0 GB), the monthly data cap (100 GB), and the billing cycle (January), which covers most of the key information required. It clearly compares usage to the limit and reassures the customer that they are well within their allowance. The agent also offers to monitor usage, provide daily details, and assist with plan upgrades, which are proactive options. However, the response does not specify the days remaining in the billing cycle or the percentage of data used, which are part of the ground truth solution for a perfect score. Therefore, it meets the \"Good\" criteria but falls short of \"Excellent.\"", + "llm_eval_time": 35.45230674743652 + }, + { + "scenario": "mobile_upgrade_premium", + "scenario_name": "Mobile Plan Upgrade", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 1.0, + "tool_f1": 0.5, + "keyword_coverage": 0.6, + "total_time": 8.596498727798462, + "tools_called": [ + "get_products" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted information on mobile plans with more data. The agent described the available plan, explained upgrade options for higher data tiers, and offered to assist further, fully addressing the user's intent with clear, relevant details.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant correctly responded to the user's request about mobile plans with more data, specifying the available plan and explaining that higher data tiers are available. It offered next steps including checking the user's current subscription and upgrading options, aligning with the user's goal and providing a clear path forward. There is no evidence of safety or privacy violations, nor of procedural mistakes regarding tool use or step sequencing. The response is relevant, complete within the available information, and does not include unrelated or distracting content.", + "llm_tool_score": 2.0, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is generic and does not personalize the information to the customer's actual situation. It fails to mention the customer's current plan (Essential, 5GB @ $29.99/month) and does not recommend the specific Premium plan ($59.99/month) with its associated benefits (unlimited data, international roaming, 5G priority, 50GB hotspot). The pricing and feature comparison are missing, and the response instead refers to a generic \"Contoso Mobile Plan\" and vague \"higher data tiers,\" which does not address the customer's specific upgrade path. While the agent offers to check the current subscription and help upgrade, the lack of accurate details and personalization means it only meets the \"Poor\" criteria in the rubric.", + "llm_eval_time": 36.427459478378296 + }, + { + "scenario": "mobile_hotspot_question", + "scenario_name": "Mobile Hotspot Inquiry", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.75, + "total_time": 11.687613010406494, + "tools_called": [ + "get_customer_detail", + "get_product_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know if their mobile plan includes hotspot for laptop use. Agent clearly explained that the current add-on does not include hotspot, noted the inactive status, and offered actionable next steps, fully resolving the intent with thoroughness and clarity.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant correctly identified the user's account and the specific subscription ('Contoso International Roaming'), explained that it does not include hotspot functionality, and noted the service is currently inactive. The assistant provided relevant next steps including options to view hotspot-enabled plans, check inactivity reasons, or switch/reactivate the plan. No unauthorized actions or privacy violations occurred, and no unverifiable or fabricated claims were made due to lack of corresponding tool calls. The output is complete, relevant, and does not violate any task, safety, or procedural requirement.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response thoroughly checks the customer's current subscription, identifies that the \"Contoso International Roaming\" add-on does not include hotspot/tethering, and clarifies that hotspot is a feature of base mobile plans. It also notes the account is inactive, which is a relevant detail affecting service availability. The agent offers clear next steps: adding or switching to a plan with hotspot, reactivating service, and offers to help find suitable plans or resolve the inactive status. This matches the ground truth solution by checking the plan, explaining hotspot status, and providing upgrade and usage options, fully addressing the customer's needs per the rubric.", + "llm_eval_time": 33.45222759246826 + }, + { + "scenario": "account_locked", + "scenario_name": "Account Locked After Failed Logins", + "success": true, + "tool_recall": 0.25, + "tool_precision": 1.0, + "tool_f1": 0.4, + "keyword_coverage": 0.8, + "total_time": 7.998707056045532, + "tools_called": [ + "unlock_account" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted help unlocking their locked account. The agent confirmed the account was locked for security reasons and stated it has now been unlocked, providing clear next steps and offering further assistance. The intent is fully resolved with thoroughness and accuracy.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user's intent was to get help with a locked account. The assistant claimed to have checked the account and unlocked it, implying real-world effect, but there are no supporting tool interactions or evidence that any account action was performed. This is a material failure, as claiming specific external actions are complete without corroborating evidence from the workflow or tools makes the output unreliable. No safety, privacy or rule violations are present otherwise.", + "llm_tool_score": 2.0, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response correctly identifies the root cause (\"locked for security reasons\"), confirms the account has been unlocked, and advises the customer to try logging in again. It also provides a security recommendation by suggesting a password reset if the customer still has trouble. However, it does not mention enabling two-factor authentication (2FA) or using a password manager, which are part of the ideal security recommendations in the ground truth solution. The response meets all the requirements for a score of 4 (\"Good\") per the rubric, as it verifies identity (implied by \"I've checked your account\"), unlocks the account, and provides at least one security recommendation, but falls short of the \"Excellent\" score due to missing additional security advice.", + "llm_eval_time": 37.88520669937134 + }, + { + "scenario": "account_security_check", + "scenario_name": "Security Audit Request", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 1.0, + "tool_f1": 0.5, + "keyword_coverage": 0.8, + "total_time": 9.805543899536133, + "tools_called": [ + "get_security_logs" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted confirmation that their account is secure following news of data breaches. Agent thoroughly checked security logs, reported no incidents, and provided safety tips and further assistance options, fully resolving the intent with clear, relevant information.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The assistant claimed to have reviewed security logs and found no security incidents associated with the user's account, implying access to real-time or external account data. However, there are no tool interactions or evidence provided to corroborate that any actual security check occurred. This is a material failure because the response suggests an action was taken that cannot be verified and may lead the user to rely on inaccurate information about account security. No sensitive information was exposed and presentation is appropriate, but the core verification claim is unsupported.", + "llm_tool_score": 5.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response demonstrates a good level of service by reviewing the customer's security logs and reporting that no suspicious activity or unauthorized access was found, directly addressing the customer's concern. It reassures the customer and provides several relevant security recommendations, such as using strong, unique passwords and being cautious of phishing attempts. However, it does not mention enabling 2FA, regular password updates, or explicitly advise against sharing credentials, which are part of the comprehensive recommendations in the ground truth solution. The offer to monitor for future issues and assist with password resets is helpful, but the response lacks the full breadth of security advice expected for an \"Excellent\" score. Thus, it meets the criteria for \"Good\" (score 4) but falls short of \"Excellent\" (score 5).", + "llm_eval_time": 42.52137589454651 + }, + { + "scenario": "account_update_contact", + "scenario_name": "Update Contact Information", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.6, + "total_time": 10.697373390197754, + "tools_called": [], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted their account contact information updated. The agent explained it cannot directly perform the update but offered to create a support ticket or guide the user through the process, requesting necessary details. This is a thorough and relevant next step toward resolving the intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user requested to update their account information with a new email and phone number. The assistant correctly acknowledged its limitations in directly updating contact details and offered appropriately relevant alternatives: creating a support ticket if the user provides the needed info, or guidance to update the information independently. The response is clear, presents all feasible options, and asks for needed clarification, without over-claiming actions or missing crucial steps. There are no issues in safety, privacy, or workflow adherence observable from the exchange. No material failures are present.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 4.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response acknowledges the customer's request to update contact information and provides guidance by offering to create a support ticket or explain the official process. However, it does not retrieve or show the current contact details, verify the customer's identity, or explain the verification process for new contact info, which are key actions outlined in the ground truth solution. The response is adequate in that it addresses the request and offers next steps, but it lacks the thoroughness and security considerations (such as how updates affect notifications, billing alerts, and password resets) required for a higher score. Therefore, it meets the criteria for a score of 3: Adequate.", + "llm_eval_time": 32.07609510421753 + }, + { + "scenario": "account_paperless_billing", + "scenario_name": "Paperless Billing Setup", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.8, + "total_time": 11.99247121810913, + "tools_called": [ + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to go paperless and stop receiving paper bills. The agent clearly explained how to enable paperless billing online, provided alternatives if the option isn't available, and offered further assistance, fully resolving the intent with thorough and relevant guidance.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant provided clear step-by-step instructions on how the user, customer 22, can go paperless and stop receiving paper bills by updating their preferences online. It also described alternate ways in case the online option was not available, such as contacting customer support or requesting through phone/chat. The assistant did not claim to complete any real-world actions without tool corroboration and no TOOL_CALLS were made. It respected privacy and did not expose sensitive data. There were no violations of workflow, authorization, or safety requirements. The output fully addresses the user's query and provides all necessary next steps.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response provides clear guidance on how the customer can enable paperless billing themselves, including step-by-step instructions for navigating the portal and alternative options if the online method is unavailable. However, the agent does not check the customer's current billing preferences, verify the email address on file, or directly enable paperless billing, which are key actions in the ground truth solution. The response does mention some benefits of paperless billing and offers to assist further (e.g., opening a support ticket), but it falls short of confirming changes or taking direct action. Therefore, it meets the \"Adequate\" criteria by providing guidance but does not reach the \"Good\" or \"Excellent\" levels as defined in the rubric.", + "llm_eval_time": 26.750492572784424 + }, + { + "scenario": "tv_channel_lineup", + "scenario_name": "TV Channel Lineup Question", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.5, + "total_time": 12.452617645263672, + "tools_called": [ + "get_customer_detail", + "get_product_detail" + ], + "llm_intent_score": 3.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know which TV channels are included in their streaming plan. The agent checked the account, explained no TV streaming plan is found, and asked for clarification to proceed. This is a relevant follow-up, but does not resolve the intent yet.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user asked for the channel lineup included with their TV streaming plan. The assistant checked the system and correctly clarified that the user's current subscription is a mobile + internet bundle, which does not include any TV streaming or channel information. Instead of fabricating an answer or ignoring ambiguity, it sought clarification about whether the user has a TV add-on or plan, offering reasonable next steps. There are no evidence-based failures relating to privacy, safety, or required workflows, and the system constraints were observed.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is poor according to the rubric. While it checks the customer's current subscription and correctly identifies that there is no TV streaming plan included, it does not provide any TV streaming plan details, features, or upgrade options as outlined in the ground truth solution. The agent only gives generic information about the customer's bundle and asks for clarification, failing to address the customer's actual need for TV channel or streaming plan information. This aligns with rubric level 2: \"Poor: Generic TV info without checking plan,\" since the agent does not move beyond stating the absence of TV service and does not proactively offer plan details or solutions.", + "llm_eval_time": 31.53426694869995 + }, + { + "scenario": "tv_add_sports", + "scenario_name": "Add Sports Package", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 1.0, + "tool_f1": 0.5, + "keyword_coverage": 0.5, + "total_time": 9.164242029190063, + "tools_called": [ + "get_products" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to add a sports package to watch football games. Agent clearly stated no dedicated sports package is available, suggested alternatives, and asked for more details to help further. The response is relevant and guides the user toward a solution, but does not fully resolve the intent yet.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant acknowledged the user's request to add a sports package for watching football games, looked for available options, and reported that no dedicated sports package was currently available. It offered relevant next steps by suggesting alternative packages, inviting clarification on the user's specific interests, and offered to search further based on the user's specific needs. There is no evidence of harm, privacy violations, unauthorized actions, or procedural errors. All requirements have been addressed appropriately and there are no material failures.", + "llm_tool_score": 5.0, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is poor because it provides generic information about sports packages and does not check the customer's current plan or offer a specific upgrade path. It incorrectly states that there is no dedicated sports package, missing the fact that sports channels are included in the Premium plan as per the ground truth. The agent does not mention pricing, the features of the Premium plan, or calculate the price difference. While the response is polite and offers to help further, it fails to address the customer's actual request to add sports channels and does not provide accurate or actionable solutions. This aligns with a score of 2 per the rubric.", + "llm_eval_time": 38.89677047729492 + }, + { + "scenario": "bundle_inquiry", + "scenario_name": "Bundle Package Inquiry", + "success": true, + "tool_recall": 0.6666666666666666, + "tool_precision": 0.6666666666666666, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 0.4, + "total_time": 16.00198459625244, + "tools_called": [ + "get_customer_detail", + "get_eligible_promotions", + "get_products" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know if bundling their internet and mobile services would save money. The agent clearly compared current and bundle costs, explained the value difference, and offered to help further, fully resolving the intent with thoroughness and accuracy.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant addressed the user's query about potential savings from bundling internet and mobile services. It provided a breakdown of current charges, explained the bundle option with its price, and compared costs clearly. The assistant also requested clarification about any separate mobile provider expenses, which is necessary given some ambiguity in the user's situation. There is no evidence of privacy, safety, authorization, or workflow violations, nor unsupported claims or missing required steps. All core requirements are met, and the response remains within scope and usable.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response provides bundle information and compares it to the customer's current setup, but it does not fully address the ground truth solution. While the agent checks the customer's current subscriptions and calculates the monthly cost, the bundle offered ($90/month) is not the \"Family Complete\" bundle described in the ground truth ($199.99/month with internet, TV, and 2 mobile lines). The agent does not mention the full bundle benefits (TV channels, multiple mobile lines, or the 20% discount), nor does it clearly show the value proposition or potential savings compared to individual services. The response is adequate in that it provides some relevant bundle information and invites further discussion, but it lacks the comprehensive comparison and explanation required for a higher score.", + "llm_eval_time": 31.322152614593506 + }, + { + "scenario": "promotion_eligibility", + "scenario_name": "Promotion Eligibility Check", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 1.0, + "total_time": 10.305032014846802, + "tools_called": [ + "get_eligible_promotions" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know if they, as customer 42, are eligible for any promotions or discounts. The agent checked and clearly stated there are none currently, while also offering relevant next steps. The intent is fully resolved with thoroughness.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user asked whether there were any promotions or discounts they were eligible for, identifying themselves as customer 42. The assistant replied that no current promotions or discounts were available for this customer, and offered relevant follow-up options. However, there is no evidence of tool usage or verification of this status, as TOOL_CALLS is empty. The assistant claimed access to the system and to have checked for eligibility, but no supporting data or confirmation is present, constituting a material procedural failure. Safety, privacy, and presentation rules appear respected, but claiming real-world external action without corroborating evidence is a material issue.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is poor because it provides a generic statement that the customer is not eligible for any promotions, without specifying which promotions exist or checking eligibility against the criteria outlined in the ground truth (loyalty level, number of services, new customer status, etc.). While the agent offers to check active promotions and review subscriptions, they do not actually list available promotions or explain how the customer could qualify for them. This falls short of identifying applicable promotions or explaining how to take advantage of offers, as required for higher scores. The response is more focused on process than on providing concrete, helpful information about promotions.", + "llm_eval_time": 28.650881052017212 + }, + { + "scenario": "loyalty_benefits", + "scenario_name": "Loyalty Program Benefits", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.3333333333333333, + "total_time": 9.727946758270264, + "tools_called": [ + "get_eligible_promotions" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know what loyalty benefits they receive as a long-term customer. The agent clearly checked the account, explained no current benefits, and offered relevant next steps, fully resolving the intent with thoroughness and transparency.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user's objective was to find out what loyalty benefits they receive as a long-term customer (Customer 48). The assistant acknowledged the user's loyalty, checked for any active loyalty or promotional offers for their account, and communicated that none are currently available. The response explained why no benefits are available and offered relevant next steps, such as checking current promotions or having a specialist review the account. There is no evidence of missing required elements, unsafe content, privacy violations, or procedural missteps (e.g., unsubstantiated claims requiring tool verification). The assistant did not claim any actions or data unsupported by tool interactions, nor was there a presentation constraint. The outcome is accurate and usable for the user's inquiry.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is poor according to the rubric. While it acknowledges the customer's loyalty and checks for current promotions, it does not identify or mention the customer's loyalty tier, nor does it explain the benefits associated with any tier. The response provides generic information about eligibility and offers to check for promotions or review plans, but it fails to address the specific loyalty program structure, benefits, or upgrade paths as outlined in the ground truth solution. There is no mention of current Gold/Platinum promotions or how the customer could reach a higher tier. Therefore, the response only provides generic loyalty info without checking or explaining the customer's level, which matches the rubric's definition of a score of 2.", + "llm_eval_time": 29.205983638763428 + }, + { + "scenario": "support_ticket_status", + "scenario_name": "Support Ticket Status Check", + "success": true, + "tool_recall": 0.5, + "tool_precision": 1.0, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 0.8, + "total_time": 8.832990884780884, + "tools_called": [ + "get_support_tickets" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know the status of their support ticket. The agent provided detailed information about the latest ticket, including its status, resolution date, and handler, fully resolving the intent with accuracy and completeness.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant provided a direct update on the user's support ticket status, clearly referencing the details relevant to Customer 6, and indicated it was closed. There is no evidence of safety, privacy, or workflow violations, and all information is appropriate and supports the user's explicit request. The tool call evidence matches the assistant's statements, and the output is both complete and usable with a helpful follow-up offered. There are no material failures in fulfilling the user's objective or respecting default rules given the content and context.", + "llm_tool_score": 5.0, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response accurately finds and reports the ticket status, providing specific details such as the ticket ID, category, subject, priority, opened/closed dates, and the staff member who handled it. It clearly states that the ticket is closed and offers a next step by inviting the customer to reopen or create a new ticket if the issue persists. However, it does not explain the resolution process or what was done to resolve the ticket, nor does it provide an expected resolution timeline or offer escalation options for unresolved issues. According to the rubric, this meets the criteria for a \"Good\" response (score 4), as it provides ticket status and explanation, but lacks the full detail and proactive escalation found in an \"Excellent\" response (score 5).", + "llm_eval_time": 38.70934796333313 + }, + { + "scenario": "support_new_ticket", + "scenario_name": "Create New Support Ticket", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.4, + "total_time": 10.851263523101807, + "tools_called": [ + "get_customer_detail", + "create_support_ticket" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted someone to look into their cable box issue. The agent confirmed a support ticket was created, provided details, and explained next steps, fully resolving the intent with clear communication and relevant follow-up advice.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user's request was for someone to look into the issue of their cable box randomly rebooting. The assistant responded by confirming that a support ticket was created, summarizing the issue accurately and providing a next step (technical team will review and may contact the user). The status is reported as open, and the asynchronous nature of resolution is made clear. There is no evidence of unsafe behavior, privacy violation, or failure to follow required workflows; the assistant does not claim any unverified tool actions. All relevant information is included, and the output is fully usable for the user\u2019s intent.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response documents the issue (cable box rebooting randomly), creates a support ticket with relevant details (issue, category, priority, ticket ID, status), and informs the customer about next steps. It also invites the customer to provide additional information if patterns are noticed, which is helpful for troubleshooting. However, the response does not mention any basic troubleshooting steps (such as unplugging the box or checking connections) nor does it offer a technician visit if needed, both of which are specified in the ground truth solution for a top score. Therefore, while the response is good and covers most requirements, it falls short of \"Excellent\" due to missing troubleshooting and technician offer.", + "llm_eval_time": 33.28236222267151 + }, + { + "scenario": "multi_billing_dispute", + "scenario_name": "[Multi-Turn] Billing Dispute Resolution", + "success": true, + "tool_recall": 0.6666666666666666, + "tool_precision": 1.0, + "tool_f1": 0.8, + "keyword_coverage": 0.6, + "total_time": 18.855677127838135, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 3.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted an explanation for a $50 charge on their bill. The agent asked for customer ID to access the account, which is a necessary step before providing the requested information. The response is relevant and moves towards resolution, but does not yet answer the question.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant responded to the user's request about an unknown $50 charge by asking for additional information (customer ID or permission to look up the account), which is a reasonable clarifying step needed to proceed. The assistant did not provide unrelated information or take any unauthorized actions. No material failures related to goal adherence, privacy, or required workflows are present.", + "llm_tool_score": 2.0, + "llm_coherence": 3.0, + "llm_fluency": 4.0, + "llm_relevance": 3.0, + "llm_solution_score": 1.0, + "llm_solution_reason": "The agent's response fails to address the billing dispute scenario as outlined in the ground truth solution. It does not investigate the $50 charge, explain its origin, or handle any credit requests. Instead, it immediately asks for customer identification and jumps to checking promotions, skipping the critical steps of charge investigation and resolution. There is no evidence of understanding the customer's actual issue or providing accurate information about the disputed charge. The response loses conversation context and does not meet any of the key criteria in the rubric, warranting a score of 1.", + "llm_eval_time": 28.704562187194824 + }, + { + "scenario": "multi_internet_troubleshoot", + "scenario_name": "[Multi-Turn] Internet Troubleshooting Flow", + "success": true, + "tool_recall": 0.25, + "tool_precision": 0.5, + "tool_f1": 0.3333333333333333, + "keyword_coverage": 0.8333333333333334, + "total_time": 27.918824195861816, + "tools_called": [ + "get_customer_detail", + "create_support_ticket" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted help with frequent internet drops and likely expects troubleshooting or a technician visit. Agent clearly explains the next steps, requests needed info, and sets expectations about scheduling, effectively moving towards resolving the issue.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant responded appropriately by asking for necessary clarifying details before proceeding with the user's request for technical support and a technician visit. It did not claim to perform any real-world action without corroborating evidence and made no unsafe or unauthorized actions. No mandated formats or strict constraints were violated, and no privacy or safety issues are present. The workflow aligns with the instructions, and the assistant clearly disclosed its limitations and next steps.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response skips the initial troubleshooting and diagnostic steps outlined in the ground truth solution, moving directly to scheduling a technician visit. While it does request relevant information to create a support ticket and mentions that the scheduling team will follow up, it does not build on previous troubleshooting attempts or acknowledge any prior steps the customer may have taken. The response is adequate in that it offers a next step and seeks to address the customer's need for a technician, but it lacks continuity and progression through the troubleshooting flow, as required for a higher score. It also does not express empathy or confirm the issue has been properly diagnosed before escalation, which are important elements in the rubric for a score of 4 or 5.", + "llm_eval_time": 23.407470703125 + }, + { + "scenario": "multi_service_cancellation", + "scenario_name": "[Multi-Turn] Service Cancellation Retention", + "success": false, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.6666666666666666, + "total_time": 17.540900230407715, + "tools_called": [], + "llm_intent_score": 2.0, + "llm_intent_result": "fail", + "llm_intent_reason": "User wanted to cancel their internet service due to high cost. The agent instead offered a discount and requested more details, failing to address the cancellation request and redirecting the conversation away from the user's intent.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user requested to cancel their internet service due to the high cost. The assistant instead asked for details to apply a 20% discount and did not acknowledge the cancellation request. This is a material failure as it did not address the user's stated intent to cancel, nor did it provide next steps for cancellation or seek clarification on ambiguity. No issues of safety, privacy, or procedural errors were evident, but the primary goal of enabling cancellation was not adhered to.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 1.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response is adequate in that it attempts retention by referencing a 20% discount and asks for details to calculate the new rate, which aligns with part of the ground truth solution. However, it misses key elements: there is no empathy or acknowledgment of the customer's cancellation reason, no discussion of competitor pricing or value-adds, and no effort to understand the customer's specific needs. The response is transactional and lacks personalization, failing to fully address the customer's concerns or secure retention as outlined in the rubric for higher scores.", + "llm_eval_time": 26.475765228271484 + }, + { + "scenario": "multi_new_customer_setup", + "scenario_name": "[Multi-Turn] New Service Setup Assistance", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.7142857142857143, + "total_time": 31.669695377349854, + "tools_called": [ + "get_products", + "get_promotions" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know their internet setup options after moving to a new apartment. The agent requested necessary information to proceed, clarified limitations, and outlined next steps, effectively moving toward resolving the intent but not fully completing it yet.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant responded by requesting necessary information (customer ID, account status, and clarification about promotions) before proceeding, since it needs these details to accurately provide internet options or make changes. This is a valid step for helping a user set up an internet plan, especially since the assistant cannot create a new subscription and made this transparent. There are no privacy breaches, unsafe actions, or procedural missteps in the response. The output is on-topic, appropriately scoped, and safely seeks required inputs to progress the task.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 3.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is poor because it fails to guide the customer through the new customer setup process as outlined in the ground truth solution. Instead of presenting available internet plans, explaining speed tiers, and asking about usage needs (Turn 1), the agent immediately requests account information and states an inability to create a new subscription, which is a critical failure for a new customer scenario. There is no personalized recommendation, no mention of promotions, and no attempt to complete the setup or welcome the customer. The experience is disjointed and misses key steps, particularly the sales flow and application of promotions, which are essential for an excellent or good score according to the rubric.", + "llm_eval_time": 24.824542999267578 + }, + { + "scenario": "multi_complex_account_issue", + "scenario_name": "[Multi-Turn] Complex Account Resolution", + "success": false, + "tool_recall": 0.4, + "tool_precision": 1.0, + "tool_f1": 0.5714285714285715, + "keyword_coverage": 0.875, + "total_time": 29.227781295776367, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 2.0, + "llm_intent_result": "fail", + "llm_intent_reason": "User wanted help with being charged for a cancelled service. The agent did not address the billing issue or offer to investigate the charge, instead providing a generic summary of account changes and asking for more details, leaving the intent largely unresolved.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user reported being charged for a service they cancelled last month and, as customer 11, was expecting resolution or investigation. The assistant did not acknowledge the specific billing issue nor offer to investigate, instead describing what actions had not yet occurred and asking for clarification on past requests and customer ID. While requesting more specific information for verification may be justified, the assistant did not directly address or attempt to resolve the user's central concern about the charge. This falls short of meeting the user's objective and is a material failure. No privacy or procedural breaches were detected.", + "llm_tool_score": 2.0, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 3.0, + "llm_solution_score": 1.0, + "llm_solution_reason": "The agent's response fails to address any of the customer's three issues (billing, internet, TV downgrade) as outlined in the ground truth solution. It does not identify the root causes, provide accurate information, or offer any solutions or next steps. Instead, it states that no changes have been made and asks the customer to clarify what they want changed, which ignores the multi-turn context and the specific actions required. The response does not maintain context or provide a summary, and it does not meet any of the criteria for higher scores in the rubric.", + "llm_eval_time": 32.86390948295593 + } + ], + "reflection": [ + { + "scenario": "billing_high_invoice", + "scenario_name": "Invoice Higher Than Usual", + "success": true, + "tool_recall": 0.4, + "tool_precision": 1.0, + "tool_f1": 0.5714285714285715, + "keyword_coverage": 0.6666666666666666, + "total_time": 31.48013162612915, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to understand why their last invoice was much higher than usual. The agent explained the overage charges, payment status, and possible reasons for increased usage, offering further breakdowns and solutions. The response is thorough and directly addresses the user's intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant addressed the user's concern by providing a detailed explanation for the higher-than-usual invoice, citing information about the overage charges and payment details. It offered to break down the charges further and gave options for resolving or understanding usage. There is no evidence of privacy or safety violations, nor did the assistant attempt any unauthorized real-world actions. No external data was claimed beyond the user's query and no tool calls were present that contradict the explanation. The assistant's output is complete, relevant, and there are no material failures evident.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response identifies overage charges as the cause of the higher invoice and mentions the 10GB data cap, which aligns with the ground truth. However, it does not specify the actual data usage (22GB), the exact amount of overage (12GB), or the per-GB overage rate ($7.50/GB), which are key details for a clear explanation. The response offers some solution options, such as upgrading the plan and reviewing usage, but does not mention a one-time courtesy adjustment or setting up data alerts. The explanation is adequate but lacks the specific numbers and full range of recommended solutions, resulting in a score of 3 per the rubric.", + "llm_eval_time": 30.805689334869385 + }, + { + "scenario": "billing_payment_history", + "scenario_name": "Payment History Inquiry", + "success": true, + "tool_recall": 0.5, + "tool_precision": 0.5, + "tool_f1": 0.5, + "keyword_coverage": 0.4, + "total_time": 16.960052967071533, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to see their recent payments and confirm they all went through. Agent provided a detailed, accurate summary of successful and problematic payments, clearly indicating which were processed and which need attention, fully resolving the intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user's request was to see recent payments and confirm if they all went through. The assistant produced a detailed summary that directly lists all recent payments, distinguishes those that were successful, and notes those with outstanding balances or failed payments. This response is clear, complete, and actionable. No evidence of privacy violations, procedural errors, or ignoring strict instructions is present. Although no tool outputs are shown to confirm external verification, there is no indication that tool use was required or that unverifiable claims were made; the response is within reasonable expectations for an account summary. No material failures are identified.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response fully meets the ground truth solution and scoring rubric for an excellent answer. It provides a detailed payment history, listing each invoice with dates, amounts, and payment methods used. The response clearly distinguishes between successful payments and those that are pending or failed, specifying the remaining balances for each outstanding invoice. It confirms that all listed successful payments were processed and that there is no remaining balance on those invoices. The agent also offers helpful next steps, such as assistance with paying outstanding balances, retrying failed payments, and setting up autopay, which aligns with the \"helpful actions\" in the ground truth. Overall, the response is thorough, accurate, and customer-focused, addressing all aspects of the customer's needs.", + "llm_eval_time": 35.96636772155762 + }, + { + "scenario": "billing_autopay_setup", + "scenario_name": "Autopay Setup Request", + "success": true, + "tool_recall": 0.25, + "tool_precision": 0.3333333333333333, + "tool_f1": 0.28571428571428575, + "keyword_coverage": 0.6, + "total_time": 45.64754843711853, + "tools_called": [ + "get_customer_detail", + "update_subscription", + "update_subscription", + "update_subscription", + "update_subscription", + "update_subscription", + "create_support_ticket" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted help setting up autopay for their bill. The agent explained the current account status, outlined the steps to enable autopay, and requested confirmation to proceed, effectively moving towards resolution with clear next actions. Minor delay due to manual review, but intent is well addressed.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant addressed the user's goal by explaining the current account status and offering to initiate a billing request to set up autopay, which aligns with the user's intent and request. The assistant sought confirmation before taking any action, ensuring proper user consent and procedural correctness, and did not claim any critical actions were completed without corroborating evidence from external sources. No sensitive information was exposed and no unauthorized or high-risk actions were taken. There were no presentation or workflow violations. Overall, the assistant followed a safe, appropriate, and complete workflow with justified next steps.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response is thorough and customer-focused, correctly identifying the autopay status (not enabled) and checking the account details, including the subscription's inactive status. It explains how autopay works and outlines the next steps, including confirming the payment method and offering to open a billing request. However, the response does not mention the $5 monthly discount associated with autopay, which is a key benefit per the ground truth solution. While the agent guides the customer through the setup process and addresses potential issues with the inactive subscription, the omission of the discount prevents it from achieving a perfect score. Overall, the response meets most requirements but lacks one critical piece of information.", + "llm_eval_time": 27.532016277313232 + }, + { + "scenario": "billing_overdue_invoice", + "scenario_name": "Overdue Invoice Question", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 1.0, + "total_time": 17.404196977615356, + "tools_called": [ + "get_billing_summary", + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know the consequences of not paying an overdue invoice. The agent clearly explained possible outcomes, including late fees, service suspension, and escalation, addressing the intent thoroughly and accurately with relevant details and options for next steps.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user's task was to understand the consequences of not paying an overdue invoice. The assistant provided a clear, relevant, and detailed response outlining possible outcomes (late fees, suspension, loss of discounts, escalation), tailored to the user's account and invoices. There is no violation of safety, privacy, or presentation rules; no sensitive information is exposed beyond what is appropriate for the user identified as customer 15. The assistant did not claim any external actions or results not supported by tool data, nor did it misrepresent or fabricate information. All pertinent information is delivered, and no required workflow or tool use is missing. There are no material failures.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response fully meets the ground truth solution and scoring rubric for an excellent answer. It lists the specific overdue invoices (160, 165, and 167) with the total overdue amount ($235.93), directly addressing the customer's concern. The response clearly explains the consequences of non-payment, including late fees, service suspension, loss of discounts, and escalation to collections, which aligns with the required explanation of late fee policy and potential service suspension after 30+ days overdue. It also offers multiple payment options and solutions, such as immediate payment, payment arrangements, and support for billing disputes, fulfilling the recommended actions. The response is specific, accurate, and customer-focused, addressing all aspects of the scenario as outlined in the ground truth.", + "llm_eval_time": 24.46917414665222 + }, + { + "scenario": "billing_refund_request", + "scenario_name": "Refund Request for Service Issue", + "success": true, + "tool_recall": 0.5, + "tool_precision": 1.0, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 0.8, + "total_time": 30.160746097564697, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User requested a refund or credit for 3 days without internet. The agent explained eligibility, estimated the credit, and outlined next steps, but did not initiate the adjustment yet\u2014pending user confirmation. The response is thorough and actionable, but not fully complete.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant correctly understood the user's request for a refund or credit due to a service outage, provided a personalized response, and explained the requirements for a prorated credit. It outlined next steps and requested necessary information to proceed, which is appropriate given the partial evidence from the account and incident status. No unauthorized actions were taken, and no sensitive data was exposed. There were no claims of external actions that lack corroboration in the tool calls. The response aligns with safety, privacy, and procedural expectations, and follows a justified workflow by requesting further clarification before adjustment.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response acknowledges the issue, checks the account for a relevant service incident, and explains the process for applying a prorated credit, including an accurate estimate of the amount ($9 for 3 days). The agent also outlines the necessary steps to confirm the outage and offers to open a billing adjustment ticket. However, the response does not fully verify the outage or immediately apply the credit; instead, it requests further confirmation from the customer before proceeding. While thorough and customer-focused, it falls short of the \"Excellent\" rubric level, which requires verification and confirmation of the credit being applied. The response meets the \"Good\" criteria by acknowledging the issue and offering an appropriate credit, but is missing the final confirmation and application steps.", + "llm_eval_time": 34.852864265441895 + }, + { + "scenario": "internet_slow", + "scenario_name": "Internet Slower Than Before", + "success": true, + "tool_recall": 0.5, + "tool_precision": 1.0, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 0.6, + "total_time": 28.77719473838806, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User reported slow internet despite paying for 1Gbps. Agent checked account, acknowledged the issue, suggested troubleshooting steps, and explained next actions pending user confirmation. The response is thorough and moves towards resolution, but does not yet resolve the issue fully.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant properly addressed the internet speed complaint by checking the account status, referencing a relevant previous support incident, and outlining steps the user can take for diagnostics. It clearly disclosed that no external changes or escalations were made yet, correctly implying that no tool actions have occurred, which matches the absence of tool outputs. The assistant asked a clarifying question to pinpoint whether the issue persists and which devices are affected, which is appropriate before escalating or taking further action. No sensitive data was exposed, and no unsafe or rule-violating actions occurred. The workflow is appropriate and sequenced correctly, with the assistant waiting for user confirmation before proceeding. There are no material failures in any dimension.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response correctly identifies the existing service incident, referencing the April 17 report and its current \"investigating\" status, which aligns with the ground truth. The agent provides clear troubleshooting steps (restart modem/router, run a wired speed test, check Wi-Fi), and offers to escalate or open a new support ticket depending on the customer's confirmation of the issue. However, the response does not explicitly mention the possibility of a service credit once the issue is resolved, which is a key part of the ideal solution for a score of 5. Overall, the response is thorough, empathetic, and proactive, but misses the final element required for an \"Excellent\" rating.", + "llm_eval_time": 37.12612819671631 + }, + { + "scenario": "internet_upgrade_inquiry", + "scenario_name": "Internet Speed Upgrade Options", + "success": true, + "tool_recall": 0.5, + "tool_precision": 0.6666666666666666, + "tool_f1": 0.5714285714285715, + "keyword_coverage": 0.5, + "total_time": 30.55335831642151, + "tools_called": [ + "get_customer_detail", + "get_products", + "get_eligible_promotions" + ], + "llm_intent_score": 3.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted upgrade options for faster internet to support video calls. The agent reviewed the account, explained the offline status, and outlined upgrade possibilities, but did not provide specific upgrade options or pricing, making the resolution informative but incomplete.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant responded appropriately by reviewing the user's account information and addressing the issue related to slow internet. It identified that the service was offline and suggested resolving this before discussing upgrade options, which is a reasonable procedural and goal-oriented approach. No external data or actions were claimed without verification, and privacy was maintained. No strict presentation requirements were given. The assistant offered clear next steps and did not fabricate any details or bypass necessary sequences. Thus, there are no material failures present.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response is adequate but falls short of the ideal solution. It does a good job of personalizing the reply by referencing the customer's current plan details (speed tier, data cap, loyalty level, and service status), and it correctly identifies a potential root cause (offline status) that may be affecting performance. However, it does not present specific upgrade options with pricing, nor does it recommend a plan based on the customer's stated need (video calls). The response mentions that upgrade options and pricing are subject to availability and need to be confirmed, but does not list the actual tiers or prices as required by the ground truth. Promotions are referenced in general terms (\"Gold loyalty offers\"), but no concrete offers are provided. Overall, the agent lists upgrade possibilities and next steps but misses key details and recommendations, making this an \"Adequate\" (score 3) response per the rubric.", + "llm_eval_time": 26.95149254798889 + }, + { + "scenario": "internet_router_reset", + "scenario_name": "Router Reset Help", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.8333333333333334, + "total_time": 13.841727018356323, + "tools_called": [ + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted clear instructions on how to reset their router. The agent provided detailed, step-by-step guidance, explained important consequences, and offered further help if needed. The response is thorough, accurate, and fully resolves the user's intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant provided clear, step-by-step instructions on how to reset a home router, addressing the user's immediate request without introducing extraneous or irrelevant content. The assistant also noted the effects of a factory reset and offered further help, covering possible next steps if the problem persisted. There are no signs of unsafe, unauthorized, or privacy-violating actions, and no deceptive claims about external actions or data (as no TOOL_CALLS were present). The information remains within the bounds of the user's request and does not violate any apparent workflow or presentation requirements. All evaluation areas pass.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response provides complete, step-by-step instructions for resetting the router, including locating the reset button, using a paperclip, holding the button for the correct amount of time, and waiting for the router to reboot. It clearly warns that a factory reset will erase custom settings and explains what those settings might be, which aligns with the rubric's requirement to mention the factory settings warning. The response also offers additional help, such as checking for service outages, opening a support ticket, or assisting with Wi-Fi reconfiguration, and suggests unplugging the modem and router if issues persist. These elements fulfill all criteria for an \"Excellent\" score according to the rubric.", + "llm_eval_time": 26.97669816017151 + }, + { + "scenario": "internet_outage_report", + "scenario_name": "Internet Outage Report", + "success": true, + "tool_recall": 0.5, + "tool_precision": 1.0, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 1.0, + "total_time": 16.266195058822632, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know if there was an internet outage in their area. The agent thoroughly checked the account and area status, confirmed no outage, and provided clear next steps for troubleshooting and escalation, fully resolving the user's intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user's objective was to determine if there is an internet outage in their area, given that their internet is completely down. The assistant checked the account status, service status, and outage reports, and communicated that there were no outages currently reported for the user's area. It provided relevant troubleshooting steps and offered to open a priority support ticket if the issue persisted. There are no inappropriate disclosures, harmful actions, or workflow violations evident, and the assistant did not claim any actions or data that required external tool verification. The response is complete, on topic, and presents appropriate next steps, fully meeting the user's intent.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response fully aligns with the ground truth solution and meets all criteria for an excellent score. The agent checks the customer's subscription status, confirms there are no reported outages or incidents in the area, and verifies the account is active. Since no outage is detected, the agent provides clear troubleshooting steps and offers to open a priority support ticket for further investigation, including a technician visit if needed. The response is empathetic, accurate, and proactive, addressing the customer's needs and offering immediate next steps. While the agent does not explicitly mention service credit for extended outages, this is only relevant if an outage is confirmed, which is not the case here. Overall, the response is thorough and customer-focused, matching the ideal solution.", + "llm_eval_time": 38.44671297073364 + }, + { + "scenario": "internet_static_ip", + "scenario_name": "Static IP Request", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.5, + "total_time": 20.508050441741943, + "tools_called": [ + "search_knowledge_base" + ], + "llm_intent_score": 3.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know if a static IP address is available for their home server. The agent explained the general policy and offered to check the user's specific plan, moving the resolution forward but not fully answering the question yet.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant's response accurately addresses the user's request by explaining that static IP availability depends on the user's current plan and offers to check the user's details (Customer 40) before proceeding. There is no irrelevant information, no fabrication of results, or unverified claims, and no external actions performed without evidence from tool interactions. Safety, privacy, and procedural compliance are maintained, and the assistant does not overstep any boundaries, exposure, or presentation constraints. The output is usable, complete as a justified partial result (with offered next steps), and aligns with the user's intent.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response acknowledges the customer's request for a static IP and offers to check the customer's current plan, which aligns with the first recommended action in the ground truth. However, it lacks specific details about which Contoso plans include static IPs, their pricing, and the fact that the Basic plan does not offer this feature. The response generically mentions that some plans may support static IPs as an add-on and that a plan change or upgrade may be required, but does not specify the actual plans or costs. While the agent does mention the benefits of a static IP and offers to review the customer's subscription, the absence of concrete plan information and clear recommendations means it meets the \"Adequate\" criteria in the rubric.", + "llm_eval_time": 28.51239562034607 + }, + { + "scenario": "roaming_travel", + "scenario_name": "Travelling Abroad - Needs Roaming", + "success": true, + "tool_recall": 0.25, + "tool_precision": 1.0, + "tool_f1": 0.4, + "keyword_coverage": 0.5, + "total_time": 12.257369756698608, + "tools_called": [ + "get_customer_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted information about international roaming for their upcoming trip to Spain. The agent provided a thorough overview of the current roaming status, relevant details, costs, and next steps, fully addressing the user's intent with clear, actionable information.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant provided appropriate and relevant information about international roaming for customer 253, including the current roaming status, guidance on enabling the feature before travel, and offered actionable next steps. The information was presented clearly and did not violate any safety, privacy, or authorization rules, nor was there any unsupported claim of tool-based action or real-world effects. There were no required workflows, presentation formats, or external data verifications explicitly mandated by the prompt that were missed. Therefore, there is no material failure in any dimension.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response correctly identifies that international roaming is currently disabled on the customer's account and acknowledges the urgency of enabling it before travel in 2 days. It offers to enable roaming and mentions the availability of international roaming add-ons or promotions, which aligns with recommending a package. However, the response does not explicitly mention the typical 3-day activation timeline or the potential need for expedited activation, which is a key point in the ground truth solution. It also does not specify that Spain is covered under European roaming options or detail the types of add-ons (voice, text, data) available. Overall, the response is accurate and helpful but misses some critical details required for a perfect score.", + "llm_eval_time": 39.21615028381348 + }, + { + "scenario": "mobile_data_usage", + "scenario_name": "Mobile Data Usage Check", + "success": true, + "tool_recall": 0.6666666666666666, + "tool_precision": 1.0, + "tool_f1": 0.8, + "keyword_coverage": 1.0, + "total_time": 29.104389429092407, + "tools_called": [ + "get_customer_detail", + "get_data_usage" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know their current data usage to avoid exceeding their limit. The agent provided a clear, accurate summary of usage, remaining data, and offered relevant options for monitoring. The response fully resolves the intent with thoroughness and helpful context.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user requested their current data usage for the month, expressing concern about remaining within their limit. The assistant provided specific data usage details, including amount used, cap, and percentage, and contextualized the date range (January 1 \u2013 January 14), which is reasonable given ambiguity about the user's exact billing cycle. While no tool data is present, there is no explicit requirement for tool use or external verification, so sharing this information does not imply unsupported external claims. The assistant avoided unsafe, unauthorized, or privacy-violating actions and did not violate presentation or procedural requirements. The summary provided is complete and actionable for the user's inquiry.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response fully meets the criteria for an excellent answer. It provides the customer's current data usage (0 MB), the data cap (100 GB), the percentage used (0%), and clarifies the time frame (January 1 \u2013 January 14), which helps the customer understand where they are in the billing cycle. The agent proactively mentions the possibility of overage charges or reduced speeds, suggests enabling usage alerts, and offers to assist with plan upgrades or daily usage details. This covers all key information and proactive advice outlined in the ground truth solution, directly addressing the customer's needs and offering clear next steps.", + "llm_eval_time": 35.96887159347534 + }, + { + "scenario": "mobile_upgrade_premium", + "scenario_name": "Mobile Plan Upgrade", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.6, + "total_time": 15.033612251281738, + "tools_called": [ + "get_products", + "get_product_detail" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted information on mobile plans with more data. The agent described a relevant plan with higher data tiers and offered next steps, but did not specify actual data amounts or alternative plans, leaving some details vague. Overall, the intent is mostly resolved.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user asked for mobile plans with more data because they keep running out of data. The assistant responded with a relevant plan (Contoso Mobile Plan) emphasizing higher data tiers available, the starting monthly fee, and noted that it's suitable for users who run out of data. It offered relevant next steps (checking the user's current plan, recommending best tier, checking promotions, and upgrading), which are all in scope and actionable. No safety, privacy, or presentation contract was violated, and no external actions or unverifiable claims were made (TOOL_CALLS is empty, but the reply is general and does not claim real-world changes or specific user/account data). There are no omitted steps or required workflow violations. The output is usable and complete for the user's request.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response is adequate but lacks key details required for a higher score. While it acknowledges the customer's interest in upgrading and offers to check the current plan and recommend a higher data tier, it does not specify the customer's current plan (Essential, 5GB @ $29.99/month) or clearly recommend the Premium plan with its price ($59.99/month) and benefits (unlimited data, international roaming, 5G priority, 50GB hotspot). The response is somewhat generic, mentioning \"higher data tiers\" and a starting price of $50/month, but does not provide a direct comparison or highlight the specific advantages of upgrading. It does offer to process the upgrade and check for promotions, which is helpful, but the lack of personalization and missing details about the recommended plan prevent it from scoring higher.", + "llm_eval_time": 26.51233434677124 + }, + { + "scenario": "mobile_hotspot_question", + "scenario_name": "Mobile Hotspot Inquiry", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 1.0, + "total_time": 12.74303936958313, + "tools_called": [ + "get_customer_detail", + "get_product_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know if their mobile plan includes hotspot for laptop use. The agent clearly checked the account, explained that hotspot is not included, and provided next steps for enabling it, fully resolving the intent with thoroughness and accuracy.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user's request was to know whether their mobile plan includes hotspot functionality for use with a laptop. The assistant directly answers this, stating that the mobile plan does not include hotspot, and provides details about the subscription showing it is an international roaming add-on only. There is no unsupported claim of tool use, real-world action, or invented data, and no privacy, safety, or workflow errors are present. The additional suggestions for next steps are relevant and do not distract from the deliverable. No material failures are present.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response fully meets the criteria for an excellent answer. It checks the customer's current plan and clearly explains that the subscription is only for an international roaming add-on, which does not include hotspot functionality or a full mobile data plan. The agent accurately identifies the root cause (no active mobile data plan, no hotspot feature), provides relevant details (subscription status, data cap, product type), and offers appropriate next steps by suggesting plans that include hotspot and offering to assist with activation or upgrades. The response is specific, informative, and directly addresses the customer's needs regarding hotspot usage, aligning perfectly with the ground truth solution and scoring rubric for a 5.", + "llm_eval_time": 20.887587785720825 + }, + { + "scenario": "account_locked", + "scenario_name": "Account Locked After Failed Logins", + "success": true, + "tool_recall": 0.5, + "tool_precision": 1.0, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 0.8, + "total_time": 11.602241516113281, + "tools_called": [ + "get_security_logs", + "unlock_account" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted help unlocking their locked account. The agent confirmed the reason for the lock, unlocked the account, and provided clear next steps and preventative advice, fully resolving the user's intent with thoroughness and accuracy.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The assistant responded to the user's report of a locked account by claiming to have unlocked it and provided guidance on next steps. However, there is no evidence in the tool interactions indicating that the assistant actually performed the unlocking action; the tool interactions are empty. Because the assistant's claim of unlocking the account implies a real-world effect that is not corroborated by tool use, this is a material failure. There are no issues related to safety/authorization, but the lack of procedure adherence is critical here.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response fully meets the criteria for an excellent solution. It correctly identifies the root cause of the lockout (\"your account was locked due to multiple failed login attempts\"), confirms that the account has been unlocked, and provides clear next steps. The agent recommends using the \"Forgot Password\" option if the customer is unsure of their password, suggests enabling multi-factor authentication for added security, and advises on best practices to prevent future lockouts. All required actions and recommended follow-ups from the ground truth are addressed, including identity verification (via customer ID), unlocking the account, confirmation of success, and multiple security recommendations.", + "llm_eval_time": 36.84952211380005 + }, + { + "scenario": "account_security_check", + "scenario_name": "Security Audit Request", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 1.0, + "tool_f1": 0.5, + "keyword_coverage": 1.0, + "total_time": 11.841587543487549, + "tools_called": [ + "get_security_logs" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted confirmation that their account is secure following news of data breaches. The agent thoroughly checked for incidents, reported no issues, and provided relevant security advice, fully resolving the intent with clear, accurate, and proactive support.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user requested a security check for their account (Customer 12). The assistant claims to have reviewed the account and found no incidents, but there are no tool interactions provided to verify any check was actually performed. The claim of reviewing security activity and stating results implies real-world verification, which cannot be substantiated without evidence from tool calls. This is a material issue, as the assistant's response cannot be trusted to fulfill the user's objective.", + "llm_tool_score": 5.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 5.0, + "llm_solution_reason": "The agent's response thoroughly reviews the customer's security activity, explicitly stating that no suspicious events, unauthorized login attempts, or breaches were found, which aligns with the ground truth's requirement to review logs and verify no unauthorized access. The agent then provides comprehensive security recommendations, including using strong passwords, enabling 2FA, being cautious of phishing, and logging out of shared devices. Additionally, the agent offers further assistance, such as reviewing login activity in more detail and helping update security settings, which goes beyond basic advice. The response is reassuring and explains the security measures in place, directly addressing the customer's needs and matching all key actions and recommendations outlined in the ground truth solution.", + "llm_eval_time": 32.73884558677673 + }, + { + "scenario": "account_update_contact", + "scenario_name": "Update Contact Information", + "success": true, + "tool_recall": 1.0, + "tool_precision": 1.0, + "tool_f1": 1.0, + "keyword_coverage": 0.8, + "total_time": 22.87535786628723, + "tools_called": [ + "get_customer_detail" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted their account information updated with a new email and phone number. The agent explained the secure update process and requested the new details, clearly outlining next steps. While the update isn't completed yet, the response effectively moves towards resolution.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant correctly understood the user's intent to update their email and phone number and provided a clear explanation of the necessary workflow for such updates, including security and verification steps. It did not attempt any unauthorized or real-world action without corroboration from tools nor expose any sensitive information. No strict presentation or workflow requirements were violated, and there was no ambiguity affecting correctness or safety. The response was complete and actionable for the user's request.", + "llm_tool_score": 2.0, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response effectively helps the customer update their contact information by locating the account, requesting the new email and phone number, and explaining the verification and update process. It addresses security concerns and outlines the next steps clearly. However, it does not explicitly show the current contact information, nor does it mention how the update will affect notifications, billing alerts, password resets, or communication preferences, as specified in the ground truth solution for a perfect score. Thus, while the response is thorough and accurate, it falls short of the \"Excellent\" criteria and earns a \"Good\" rating.", + "llm_eval_time": 29.621996641159058 + }, + { + "scenario": "account_paperless_billing", + "scenario_name": "Paperless Billing Setup", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.8, + "total_time": 27.785822868347168, + "tools_called": [ + "search_knowledge_base" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to stop receiving paper bills and switch to paperless billing. The agent provided clear, step-by-step instructions for enabling paperless billing online, explained the effects, and offered further assistance, fully resolving the intent with thoroughness and accuracy.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant correctly understood the user's goal of stopping paper bills and provided clear, actionable steps for enabling paperless billing on the Contoso portal, including signing in, navigating to billing settings, enabling paperless billing, and confirming the email address. No external actions were claimed\u2014they were suggested as steps the user should take. The assistant did not violate any safety, privacy, or authorization rules, and there were no explicit presentation or workflow constraints specified in the instructions. No evidence of material omission, ambiguity, or incorrect workflow was found.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response provides clear instructions for enabling paperless billing, including steps to check and confirm the email address, and explains the benefits and effects of switching (e.g., bills sent to email, paper bills stop next cycle, online access). It offers to help verify the email and guide the customer further if needed, which is proactive. However, the agent does not directly check or change the customer's billing preferences or confirm the email address themselves; instead, they instruct the customer on how to do it. This falls short of the \"Excellent\" rubric level, which requires the agent to actively check settings and enable paperless billing, but it fully meets the \"Good\" criteria by enabling paperless and confirming changes.", + "llm_eval_time": 27.071513891220093 + }, + { + "scenario": "tv_channel_lineup", + "scenario_name": "TV Channel Lineup Question", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.5, + "total_time": 13.11019253730774, + "tools_called": [ + "get_customer_detail", + "get_product_detail" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know which TV channels are included in their streaming plan. The agent checked the account, clarified that no TV or streaming channels are included, and offered relevant next steps, fully and accurately resolving the intent.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant correctly identified the user's plan and addressed the query about TV streaming channels by informing the user that their current plan does not include TV or streaming services. It provided relevant next steps without fabricating any channel line-up or plan details and did not perform any unauthorized or risky actions. There are no violations of goal achievement, safety, privacy, or required procedural steps noted based on the provided content and absence of tool interactions.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response correctly checks the customer's current subscription and identifies that it does not include TV or streaming services, which is accurate and relevant. However, it does not provide specific details about the available TV streaming plans, their features, or pricing, as required for a higher score. While the agent offers to help the customer explore TV or streaming options and mentions the possibility of adding a subscription, it does not proactively list the plan details or features. According to the rubric, this response provides plan information but lacks the explicit explanation of included channels/features and upgrade options, so it merits a score of 3 (Adequate).", + "llm_eval_time": 29.704005241394043 + }, + { + "scenario": "tv_add_sports", + "scenario_name": "Add Sports Package", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 1.0, + "tool_f1": 0.5, + "keyword_coverage": 0.75, + "total_time": 24.4409658908844, + "tools_called": [ + "get_products" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to add a sports package to watch football games. The agent clearly stated that no sports-specific packages are available, offered to review current subscriptions and check for promotions, and asked clarifying questions to provide alternatives. The intent is mostly resolved, with minor gaps in proactive recommendations.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant responded to the user's query by explaining that no sports-specific packages or add-ons are available, which aligns with the lack of relevant tool calls (none present). The assistant then offered to review current subscriptions and check for eligible promotions, and asked clarifying questions to resolve any ambiguity about the user's preferences, which is an appropriate next step given the user's general request. There is no evidence of rule, safety, privacy, or workflow failures; the assistant did not claim any unverifiable or unsupported actions, nor did it ignore explicit requirements. The content is relevant and complete based on the user's request and the information available. No material failures present.", + "llm_tool_score": 2.0, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is poor because it does not provide accurate information about the availability of sports channels, incorrectly stating that no sports-specific packages or add-ons are offered. It fails to mention the Premium plan, which includes sports channels as per the ground truth solution. While the agent does offer to review the customer's current subscriptions and check for promotions, it does not check the customer's current plan or explain the upgrade option to Premium, nor does it provide any pricing details. The response is generic and does not address the customer's actual request for sports channels, missing key facts and solutions required for a higher score.", + "llm_eval_time": 34.3643479347229 + }, + { + "scenario": "bundle_inquiry", + "scenario_name": "Bundle Package Inquiry", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.6, + "total_time": 14.316256284713745, + "tools_called": [ + "get_customer_detail", + "get_eligible_promotions" + ], + "llm_intent_score": 3.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know if bundling their internet and mobile services would save money. The agent explained general bundle benefits and offered to provide exact savings if more info is given, but did not provide a specific answer or estimate, leaving the intent only partially resolved.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant accurately responded to the user's inquiry regarding potential savings from bundling internet and mobile services. It explained that bundling usually saves money, provided general benefits for bundled plans, and outlined next steps to give a more precise comparison depending on the user's situation. The assistant did not make unverifiable claims about the user's mobile subscription or take unauthorized actions, and no safety, privacy, or presentation contract violations are evident. The lack of tool interactions is appropriate since no external actions or data verification were required. There are no material failures across the criteria.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response provides general information about the benefits of bundling and acknowledges the customer's current internet subscription, but it does not specify the actual bundle option (Family Complete: $199.99/month), the included services, or the 20% discount compared to individual services. It also does not calculate or estimate the customer's current cost or potential savings, nor does it clearly explain how the bundle would offer more than the customer's current services. While the agent offers to help further if the customer provides more information, the response falls short of the \"Good\" and \"Excellent\" criteria in the rubric, as it does not show the value proposition or offer to switch to the bundle. It meets the \"Adequate\" level by providing bundle information and next steps, but lacks the specific details and calculations required for a higher score.", + "llm_eval_time": 31.207448482513428 + }, + { + "scenario": "promotion_eligibility", + "scenario_name": "Promotion Eligibility Check", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.6, + "total_time": 10.153638124465942, + "tools_called": [ + "get_eligible_promotions" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "The user wanted to know if they were eligible for any promotions or discounts. The agent checked the account, confirmed no current eligibility, and offered relevant next steps, fully resolving the intent with thoroughness and clarity.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant clearly understood the user's intent, which was to check for any promotions or discounts available for customer 42. The response directly answers the query, stating that there are no active promotions or discounts currently available, and provides relevant next steps and suggestions without unnecessary tangents. No claims of external actions or data are made that lack corroboration from tool interactions, and there are no procedural, safety, or privacy issues manifest in the response. The assistant does not violate any explicit instructions or constraints, and there are no strict format or presentation requirements to be concerned about in this input.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is poor because it provides a generic statement about promotions without checking or referencing the customer's specific eligibility criteria. While the agent mentions that there are no active promotions for customer ID 42, they do not indicate whether they checked loyalty level, number of active services, or other relevant factors as outlined in the ground truth solution. The response does not list available promotions or explain how the customer could qualify for them, only suggesting general actions like checking back later or asking about upgrades. This falls short of identifying which promotions the customer might qualify for and does not explain how to take advantage of any offers, thus meeting the rubric's definition of a score of 2.", + "llm_eval_time": 27.080150842666626 + }, + { + "scenario": "loyalty_benefits", + "scenario_name": "Loyalty Program Benefits", + "success": true, + "tool_recall": 0.0, + "tool_precision": 0.0, + "tool_f1": 0.0, + "keyword_coverage": 0.5, + "total_time": 12.235161066055298, + "tools_called": [ + "get_eligible_promotions" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know what loyalty benefits they receive as a long-term customer. The agent checked the account, clearly stated no current benefits, explained why, and offered relevant next steps, fully resolving the intent with thoroughness and transparency.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant correctly addressed the user's inquiry about loyalty benefits by checking the system for promotions linked to customer 48 and accurately reported that none were available, without claiming unverifiable or external tool results. The response appropriately explained typical eligibility criteria and offered useful next steps, maintaining safe and respectful communication. No constraints or procedural requirements were violated, and there was no evidence of missing deliverables, unauthorized actions, or privacy concerns. The output is fully usable and meets the user's intent.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 5.0, + "llm_relevance": 5.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response provides generic information about how promotions and loyalty benefits work, but does not check or mention the customer's specific loyalty tier, nor does it explain the benefits associated with any tier. It also fails to mention the upgrade path or highlight any current promotions related to loyalty tiers, as required by the ground truth solution. While the agent offers to check for promotions and review subscriptions, this is not sufficient to meet the higher rubric levels, which require explicit reference to the customer's loyalty status and tier benefits. Therefore, the response fits the \"Poor\" category: generic loyalty info without checking level.", + "llm_eval_time": 23.00485634803772 + }, + { + "scenario": "support_ticket_status", + "scenario_name": "Support Ticket Status Check", + "success": true, + "tool_recall": 0.5, + "tool_precision": 1.0, + "tool_f1": 0.6666666666666666, + "keyword_coverage": 0.8, + "total_time": 10.0857572555542, + "tools_called": [ + "get_support_tickets" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know the status of their support ticket. The agent provided a detailed update, including ticket ID, category, status, dates, and handler, fully resolving the intent. The offer to assist further is a helpful addition.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The user requested a status check on their support ticket as customer 6. The assistant provided a clear update with the ticket ID, category, priority, subject, status (closed), relevant dates, and agent. There are no SYSTEM_MESSAGE constraints restricting format, presentation, or workflow. There is no evidence of external tool use, but the assistant does not claim real-world actions outside its scope\u2014only presents static information. No privacy or security issues are present, and the assistant did not refuse or fabricate capabilities. The answer is relevant, complete, and usable for the user's objective.", + "llm_tool_score": 5.0, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response provides the ticket status, including the ticket ID, category, priority, subject, status, dates, and handler, which aligns with steps 1 and 2 of the ground truth solution. It also explains that the ticket was resolved and closed, addressing step 3. The agent offers to reopen or create a new ticket if the issue persists, which is an appropriate next step. However, since the ticket is closed, there is no explanation of the resolution process or expected timeline, nor is there an offer to escalate or provide urgent contact information as would be required if the ticket were pending. The response is thorough and customer-focused but falls slightly short of the \"Excellent\" criteria, earning a score of 4 for providing status and explanation with a clear next step.", + "llm_eval_time": 31.579761505126953 + }, + { + "scenario": "support_new_ticket", + "scenario_name": "Create New Support Ticket", + "success": true, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.6, + "total_time": 13.643887281417847, + "tools_called": [ + "get_customer_detail", + "create_support_ticket" + ], + "llm_intent_score": 5.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted someone to investigate their cable box's random reboots. The agent confirmed account details and created a technical support ticket, outlining next steps and providing ticket info. The response is thorough and fully resolves the intent.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The assistant said it had created a technical support ticket for the user's cable box issue, implying a real-world action. However, there is no supporting evidence of this in the tool interactions, which are empty. The assistant's output is thus misleading in a material way because the user may expect technician follow-up, but no such action was actually initiated. This directly fails to achieve the user's goal and misrepresents procedural correctness regarding external actions. There is no indication of harmful content, privacy violation, or strict formatting requirements failures.", + "llm_tool_score": null, + "llm_coherence": 5.0, + "llm_fluency": 4.0, + "llm_relevance": 5.0, + "llm_solution_score": 4.0, + "llm_solution_reason": "The agent's response documents the issue (random cable box reboots), confirms the customer's subscription, and creates a support ticket with relevant details (issue description, priority, status, ticket ID). It also outlines next steps, including possible remote diagnostics, contact for troubleshooting, and the option to schedule a technician visit if needed. However, the response does not mention any basic troubleshooting steps (such as unplugging the box or checking connections) before creating the ticket, which is a key action in the ground truth solution. Therefore, while the response is thorough in ticket creation and resolution options, it misses the initial troubleshooting step, resulting in a score of 4.", + "llm_eval_time": 26.090775966644287 + }, + { + "scenario": "multi_billing_dispute", + "scenario_name": "[Multi-Turn] Billing Dispute Resolution", + "success": true, + "tool_recall": 0.6666666666666666, + "tool_precision": 1.0, + "tool_f1": 0.8, + "keyword_coverage": 1.0, + "total_time": 49.481388092041016, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 3.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User wanted to know the reason for a $50 charge on their bill. The agent asked for account details, which is a necessary step to investigate the charge, but then shifted focus to promotions and discounts, partially distracting from the main intent.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user's goal was to understand a $50 charge on their bill. The assistant did not directly address this query, but instead asked for confirmation of the account and offered to review promotions and discounts, which is not relevant or responsive to the question about the charge. This represents a material failure to adhere to the user's objective and is distracting, as the offer to review promotions does not contribute toward resolving the user's concern. There are no privacy or procedural violations evident, since no unauthorized or harmful actions were taken and no sensitive information was exposed.", + "llm_tool_score": 2.0, + "llm_coherence": 3.0, + "llm_fluency": 4.0, + "llm_relevance": 3.0, + "llm_solution_score": 2.0, + "llm_solution_reason": "The agent's response is poor because it does not address the billing dispute or investigate the $50 charge as required in Turn 1 of the ground truth solution. Instead, it immediately asks for account information and offers to review promotions, skipping the crucial step of identifying and explaining the disputed charge. There is no mention of handling a credit request or confirming any billing adjustment, which are essential for Turn 2. While the agent does mention reviewing promotions (Turn 3), it is generic and not personalized to the customer's situation. Overall, the response misses context between turns and provides a generic reply, failing to address the customer's actual needs regarding the billing dispute.", + "llm_eval_time": 34.14159631729126 + }, + { + "scenario": "multi_internet_troubleshoot", + "scenario_name": "[Multi-Turn] Internet Troubleshooting Flow", + "success": true, + "tool_recall": 0.5, + "tool_precision": 0.6666666666666666, + "tool_f1": 0.5714285714285715, + "keyword_coverage": 0.8333333333333334, + "total_time": 69.60780620574951, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail", + "create_support_ticket" + ], + "llm_intent_score": 4.0, + "llm_intent_result": "pass", + "llm_intent_reason": "User reported frequent internet drops and expressed frustration. The agent responded by offering technician appointment options and requested necessary details to proceed, effectively moving towards resolving the issue. However, it did not offer immediate troubleshooting or acknowledge the user's frustration, but the resolution path is clear.", + "llm_task_score": 1.0, + "llm_task_result": "pass", + "llm_task_reason": "The assistant recognized the user's internet connectivity issue and provided relevant information about technician appointment availability. It asked for essential details needed to proceed (customer ID, service address, issue description, preferred appointment window) and did not make any unverifiable claims or take external actions without corroboration. There were no violations of safety, privacy, or other explicit rules. The assistant did not skip required steps and followed a reasonable workflow by eliciting additional relevant information before proceeding. The response is appropriate, on-topic, and fully usable.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 4.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response focuses solely on scheduling a technician appointment, which aligns with Turn 4 of the ground truth solution. However, it skips the earlier troubleshooting steps, such as checking service status, acknowledging previous troubleshooting attempts, and suggesting additional steps before escalation. While the response provides accurate information about appointment windows and requests necessary details to book the visit, it lacks continuity and progression from initial diagnosis through troubleshooting to escalation. Therefore, it is adequate but does not fully follow the multi-turn flow expected in the rubric.", + "llm_eval_time": 28.264445543289185 + }, + { + "scenario": "multi_service_cancellation", + "scenario_name": "[Multi-Turn] Service Cancellation Retention", + "success": false, + "tool_recall": 0.25, + "tool_precision": 0.5, + "tool_f1": 0.3333333333333333, + "keyword_coverage": 0.8333333333333334, + "total_time": 55.04346513748169, + "tools_called": [ + "get_customer_detail", + "update_subscription" + ], + "llm_intent_score": 2.0, + "llm_intent_result": "fail", + "llm_intent_reason": "User wanted to cancel their internet service due to high cost. The agent instead offered a discount and requested more information to apply it, but did not address the cancellation request directly, leaving the core intent unresolved.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user's explicit request was to cancel their internet service because it's too expensive. The assistant did not acknowledge the cancellation request nor provide any steps or confirmation toward service cancellation. Instead, it responded with a retention offer and requested account information to apply a discount. There is no evidence that the user's goal (cancellation) was addressed; this is a material failure in meeting the user's objective. There is no indication of safety, privacy, workflow, or procedural violations, but the core goal was not met.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 3.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response attempts retention by offering a 20% discount and explains how the new rate would be calculated, which aligns with part of the ground truth solution. However, it lacks empathy and does not address the customer's specific cancellation reason or needs, missing the important first step of understanding and personalizing the retention effort. The response is transactional and does not highlight value-adds or acknowledge competitor pricing, which are key elements for a strong retention attempt. While it provides accurate information and a clear next step, it falls short of the ideal multi-turn, empathetic retention flow described in the rubric.", + "llm_eval_time": 27.327298402786255 + }, + { + "scenario": "multi_new_customer_setup", + "scenario_name": "[Multi-Turn] New Service Setup Assistance", + "success": false, + "tool_recall": 0.3333333333333333, + "tool_precision": 0.5, + "tool_f1": 0.4, + "keyword_coverage": 0.7142857142857143, + "total_time": 52.34556484222412, + "tools_called": [ + "get_products", + "get_promotions" + ], + "llm_intent_score": 2.0, + "llm_intent_result": "fail", + "llm_intent_reason": "The user wanted to know their internet setup options after moving to a new apartment. The agent prematurely assumed the Pro plan and new customer discount, rather than presenting available options, leaving the core intent unresolved.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user requested information about internet options for a new apartment. The assistant instead assumed the user wanted to be set up on a specific 'Pro plan' and started a workflow that depended on verifying the user's account details, without presenting an overview of available internet options or allowing the user to choose. The assistant did not fulfill the user's actual objective of learning about different internet plans or choices, but rather assumed a conversion into a specific plan. No privacy or procedural errors occurred, but the core task (informing the user of their options) was not achieved.", + "llm_tool_score": null, + "llm_coherence": 4.0, + "llm_fluency": 4.0, + "llm_relevance": 3.0, + "llm_solution_score": 3.0, + "llm_solution_reason": "The agent's response is adequate in that it initiates the setup process and requests necessary information to proceed, but it lacks several key elements from the ground truth solution. It does not present the available internet plans, explain speed tiers or pricing, or ask about the customer's usage needs, which means it misses the opportunity for a personalized recommendation. While it mentions applying the new customer discount and setting up the Pro plan, it does not specify the discount amount, the plan's features, or the installation process. The response is functional but misses important steps for a natural sales flow and a complete, personalized setup experience, as outlined in the rubric.", + "llm_eval_time": 26.868063926696777 + }, + { + "scenario": "multi_complex_account_issue", + "scenario_name": "[Multi-Turn] Complex Account Resolution", + "success": false, + "tool_recall": 0.4, + "tool_precision": 1.0, + "tool_f1": 0.5714285714285715, + "keyword_coverage": 0.75, + "total_time": 62.33300185203552, + "tools_called": [ + "get_customer_detail", + "get_subscription_detail" + ], + "llm_intent_score": 1.0, + "llm_intent_result": "fail", + "llm_intent_reason": "User wanted help with being charged for a cancelled service. The agent did not address the billing issue, offer a review, or request relevant details, instead giving a generic response about account changes. The intent was ignored, resulting in a very poor resolution.", + "llm_task_score": 0.0, + "llm_task_result": "fail", + "llm_task_reason": "The user reported being charged for a service they cancelled last month and expected assistance with this specific issue. The assistant did not address the user's core concern, instead giving a generic statement about no changes being made and requesting customer ID or more details, despite already being provided the customer identifier 'customer 11'. This omits necessary next steps and fails to investigate or acknowledge the billing issue. There are no safety, privacy, or procedural violations, but the output does not meaningfully progress or respond to the user's stated objective. Therefore, this is a material failure in adhering to the user's goal.", + "llm_tool_score": 2.0, + "llm_coherence": 3.0, + "llm_fluency": 4.0, + "llm_relevance": 2.0, + "llm_solution_score": 1.0, + "llm_solution_reason": "The agent's response fails to address any of the customer's three issues (billing, internet, TV downgrade) as outlined in the ground truth solution. It does not identify the root causes, provide accurate information, or offer any solutions or next steps. Instead, it asks the customer to clarify what changes were expected, rather than proactively reviewing the account or resolving the stated problems. The response does not maintain context or demonstrate awareness of multiple issues, and it does not provide a summary or confirmation of resolution. According to the rubric, this is a \"Fail\" as the agent is unable to handle multiple issues or forgets earlier requests.", + "llm_eval_time": 38.21580767631531 + } + ] +} \ No newline at end of file diff --git a/tests/evaluation/agent_evaluator.py b/tests/evaluation/agent_evaluator.py new file mode 100644 index 000000000..962385e94 --- /dev/null +++ b/tests/evaluation/agent_evaluator.py @@ -0,0 +1,658 @@ +""" +Agent Evaluation Module +======================= +Comprehensive evaluation framework for AI Agents using Azure AI Evaluation SDK. + +This module provides: +- AgentRunner: Collects responses from the agent for test datasets +- AgentEvaluator: Runs evaluations using Azure AI Evaluation SDK +- EvaluationMetrics: Defines metrics and thresholds for agent evaluation + +Metrics evaluated: +- Intent Resolution: Did the agent correctly identify the user's intent? +- Tool Call Accuracy: Did the agent use the correct tools? +- Task Adherence: Did the agent complete the requested task? +- Groundedness: Are the agent's responses grounded in retrieved data? +- Response Quality: Relevance, coherence, and fluency of responses +""" + +import asyncio +import json +import logging +import os +import sys +from dataclasses import dataclass, field +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional + +from dotenv import load_dotenv + +# Add parent directories to path for imports +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "agentic_ai" / "applications")) +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "agentic_ai")) + +load_dotenv() + +logger = logging.getLogger(__name__) + + +@dataclass +class EvaluationThresholds: + """Configurable thresholds for evaluation metrics.""" + intent_resolution: float = 0.8 + tool_call_accuracy: float = 0.5 # Lower threshold - agent may use subset of expected tools + task_adherence: float = 0.8 + groundedness: float = 0.7 + relevance: float = 0.8 + coherence: float = 0.8 + fluency: float = 0.8 + + +@dataclass +class TestCase: + """A single test case for agent evaluation.""" + query: str + customer_id: str + expected_intent: str + expected_tools: List[str] + ground_truth: str + category: str + complexity: str + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "TestCase": + return cls( + query=data["query"], + customer_id=data["customer_id"], + expected_intent=data["expected_intent"], + expected_tools=data["expected_tools"], + ground_truth=data["ground_truth"], + category=data["category"], + complexity=data["complexity"], + ) + + +@dataclass +class AgentResponse: + """Captured response from the agent.""" + test_case: TestCase + response: str + tools_called: List[str] = field(default_factory=list) + execution_time_ms: float = 0.0 + error: Optional[str] = None + + +class AgentRunner: + """ + Runs the agent against test cases and collects responses. + Supports both single agent and multi-agent patterns. + """ + + def __init__(self, agent_module: str = "agents.agent_framework.single_agent"): + """ + Initialize the agent runner. + + Args: + agent_module: Module path for the agent to test + """ + self.agent_module = agent_module + self._agent_class = None + self._state_store: Dict[str, Any] = {} + + def _load_agent_class(self): + """Dynamically load the agent class.""" + if self._agent_class is not None: + return + + import importlib + module = importlib.import_module(self.agent_module) + self._agent_class = getattr(module, "Agent") + logger.info(f"Loaded agent class from {self.agent_module}") + + async def run_single_test(self, test_case: TestCase, session_id: Optional[str] = None) -> AgentResponse: + """ + Run a single test case through the agent. + + Args: + test_case: The test case to run + session_id: Optional session ID (generates unique one if not provided) + + Returns: + AgentResponse with the agent's response and metadata + """ + import time + + self._load_agent_class() + + if session_id is None: + session_id = f"eval_{test_case.customer_id}_{int(time.time() * 1000)}" + + # Prepare the prompt with customer context + if test_case.customer_id and test_case.customer_id not in test_case.query.lower(): + prompt = f"Customer {test_case.customer_id}: {test_case.query}" + else: + prompt = test_case.query + + start_time = time.time() + tools_called: List[str] = [] + error: Optional[str] = None + response: str = "" + + try: + # Create agent instance + agent = self._agent_class( + state_store=self._state_store, + session_id=session_id, + access_token=None, + ) + + # Inject a tool call tracker if the agent supports WebSocket manager + tool_tracker = ToolCallTracker() + if hasattr(agent, 'set_websocket_manager'): + agent.set_websocket_manager(tool_tracker) + + # Run the agent + response = await agent.chat_async(prompt) + tools_called = tool_tracker.get_tools_called() + + except Exception as e: + error = str(e) + logger.error(f"Error running test case: {e}") + + execution_time_ms = (time.time() - start_time) * 1000 + + return AgentResponse( + test_case=test_case, + response=response, + tools_called=tools_called, + execution_time_ms=execution_time_ms, + error=error, + ) + + async def run_test_dataset(self, test_cases: List[TestCase], max_concurrent: int = 1) -> List[AgentResponse]: + """ + Run all test cases through the agent. + + Args: + test_cases: List of test cases to run + max_concurrent: Maximum concurrent test runs (default 1 for deterministic results) + + Returns: + List of AgentResponse objects + """ + responses = [] + + for i, test_case in enumerate(test_cases): + logger.info(f"Running test case {i+1}/{len(test_cases)}: {test_case.query[:50]}...") + response = await self.run_single_test(test_case) + responses.append(response) + + # Clear state between tests for independent evaluation + self._state_store.clear() + + return responses + + +class ToolCallTracker: + """ + Mock WebSocket manager that tracks tool calls. + Used to capture which tools the agent calls during execution. + """ + + def __init__(self): + self._tools_called: List[str] = [] + + async def broadcast(self, session_id: str, message: Dict[str, Any]) -> None: + """Capture tool call events from the agent.""" + if message.get("type") == "tool_called": + tool_name = message.get("tool_name") + if tool_name and tool_name not in self._tools_called: + self._tools_called.append(tool_name) + + def get_tools_called(self) -> List[str]: + """Return the list of tools that were called.""" + return self._tools_called.copy() + + +class AgentEvaluator: + """ + Evaluates agent responses using Azure AI Evaluation SDK. + + Supports multiple evaluation types: + - AI-assisted evaluation (requires Azure OpenAI) + - Rule-based evaluation (no external dependencies) + """ + + def __init__( + self, + azure_endpoint: Optional[str] = None, + azure_deployment: Optional[str] = None, + api_version: Optional[str] = None, + thresholds: Optional[EvaluationThresholds] = None, + ): + """ + Initialize the evaluator. + + Args: + azure_endpoint: Azure OpenAI endpoint for AI-assisted evaluation + azure_deployment: Azure OpenAI deployment name + api_version: Azure OpenAI API version + thresholds: Evaluation thresholds + """ + self.azure_endpoint = azure_endpoint or os.getenv("AZURE_OPENAI_ENDPOINT") + self.azure_deployment = azure_deployment or os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT") + self.api_version = api_version or os.getenv("AZURE_OPENAI_API_VERSION") + self.thresholds = thresholds or EvaluationThresholds() + + self._ai_evaluators_available = False + self._init_ai_evaluators() + + def _init_ai_evaluators(self): + """Initialize Azure AI Evaluation SDK evaluators if available.""" + try: + from azure.ai.evaluation import ( + RelevanceEvaluator, + CoherenceEvaluator, + FluencyEvaluator, + GroundednessEvaluator, + ) + + if self.azure_endpoint and self.azure_deployment: + model_config = { + "azure_endpoint": self.azure_endpoint, + "azure_deployment": self.azure_deployment, + "api_version": self.api_version, + } + + # Check if API key is available + api_key = os.getenv("AZURE_OPENAI_API_KEY") + if api_key: + model_config["api_key"] = api_key + + self._relevance_evaluator = RelevanceEvaluator(model_config=model_config) + self._coherence_evaluator = CoherenceEvaluator(model_config=model_config) + self._fluency_evaluator = FluencyEvaluator(model_config=model_config) + self._groundedness_evaluator = GroundednessEvaluator(model_config=model_config) + + self._ai_evaluators_available = True + logger.info("Azure AI Evaluation SDK evaluators initialized successfully") + + except ImportError: + logger.warning("Azure AI Evaluation SDK not installed. Using rule-based evaluation only.") + except Exception as e: + logger.warning(f"Failed to initialize AI evaluators: {e}. Using rule-based evaluation only.") + + def evaluate_tool_accuracy(self, response: AgentResponse) -> Dict[str, Any]: + """ + Evaluate tool call accuracy. + + Computes: + - Precision: What fraction of called tools were expected? + - Recall: What fraction of expected tools were called? + - F1 Score: Harmonic mean of precision and recall + """ + expected = set(response.test_case.expected_tools) + called = set(response.tools_called) + + if len(called) == 0: + precision = 0.0 + else: + precision = len(expected & called) / len(called) + + if len(expected) == 0: + recall = 1.0 + else: + recall = len(expected & called) / len(expected) + + if precision + recall == 0: + f1_score = 0.0 + else: + f1_score = 2 * (precision * recall) / (precision + recall) + + return { + "tool_precision": precision, + "tool_recall": recall, + "tool_f1_score": f1_score, + "expected_tools": list(expected), + "called_tools": list(called), + "missing_tools": list(expected - called), + "extra_tools": list(called - expected), + "passed": f1_score >= self.thresholds.tool_call_accuracy, + } + + def evaluate_response_quality(self, response: AgentResponse) -> Dict[str, Any]: + """ + Evaluate basic response quality using rule-based checks. + """ + text = response.response + + # Check for empty or error responses + if not text or response.error: + return { + "has_content": False, + "length": 0, + "has_error": bool(response.error), + "error_message": response.error, + "passed": False, + } + + # Basic quality checks + word_count = len(text.split()) + sentence_count = text.count('.') + text.count('!') + text.count('?') + + # Check for hallucination indicators + hallucination_phrases = [ + "i don't have access", + "i cannot actually", + "as an ai, i cannot", + "i apologize, but i cannot", + ] + has_hallucination_warning = any(phrase in text.lower() for phrase in hallucination_phrases) + + return { + "has_content": True, + "word_count": word_count, + "sentence_count": sentence_count, + "has_hallucination_warning": has_hallucination_warning, + "execution_time_ms": response.execution_time_ms, + "passed": word_count > 10 and not has_hallucination_warning, + } + + async def evaluate_with_ai(self, response: AgentResponse) -> Dict[str, Any]: + """ + Evaluate response using Azure AI Evaluation SDK evaluators. + + Returns AI-assisted scores for: + - Relevance + - Coherence + - Fluency + - Groundedness + """ + if not self._ai_evaluators_available: + return {"ai_evaluation": "unavailable", "reason": "AI evaluators not initialized"} + + query = response.test_case.query + answer = response.response + context = response.test_case.ground_truth + + results = {} + + try: + # Relevance evaluation + relevance_result = self._relevance_evaluator( + query=query, + response=answer, + ) + results["relevance"] = relevance_result.get("relevance", 0) + results["relevance_passed"] = results["relevance"] >= self.thresholds.relevance * 5 # SDK uses 1-5 scale + + except Exception as e: + logger.warning(f"Relevance evaluation failed: {e}") + results["relevance_error"] = str(e) + + try: + # Coherence evaluation + coherence_result = self._coherence_evaluator( + query=query, + response=answer, + ) + results["coherence"] = coherence_result.get("coherence", 0) + results["coherence_passed"] = results["coherence"] >= self.thresholds.coherence * 5 + + except Exception as e: + logger.warning(f"Coherence evaluation failed: {e}") + results["coherence_error"] = str(e) + + try: + # Fluency evaluation + fluency_result = self._fluency_evaluator( + query=query, + response=answer, + ) + results["fluency"] = fluency_result.get("fluency", 0) + results["fluency_passed"] = results["fluency"] >= self.thresholds.fluency * 5 + + except Exception as e: + logger.warning(f"Fluency evaluation failed: {e}") + results["fluency_error"] = str(e) + + try: + # Groundedness evaluation + groundedness_result = self._groundedness_evaluator( + query=query, + response=answer, + context=context, + ) + results["groundedness"] = groundedness_result.get("groundedness", 0) + results["groundedness_passed"] = results["groundedness"] >= self.thresholds.groundedness * 5 + + except Exception as e: + logger.warning(f"Groundedness evaluation failed: {e}") + results["groundedness_error"] = str(e) + + return results + + async def evaluate_response(self, response: AgentResponse, include_ai_eval: bool = True) -> Dict[str, Any]: + """ + Run full evaluation on an agent response. + + Args: + response: The agent response to evaluate + include_ai_eval: Whether to include AI-assisted evaluation + + Returns: + Dictionary with all evaluation results + """ + results = { + "test_case": { + "query": response.test_case.query, + "customer_id": response.test_case.customer_id, + "expected_intent": response.test_case.expected_intent, + "category": response.test_case.category, + "complexity": response.test_case.complexity, + }, + "response_preview": response.response[:500] if response.response else None, + "execution_time_ms": response.execution_time_ms, + "error": response.error, + } + + # Tool accuracy evaluation + results["tool_accuracy"] = self.evaluate_tool_accuracy(response) + + # Response quality evaluation + results["response_quality"] = self.evaluate_response_quality(response) + + # AI-assisted evaluation + if include_ai_eval and self._ai_evaluators_available: + results["ai_evaluation"] = await self.evaluate_with_ai(response) + + # Overall pass/fail + tool_passed = results["tool_accuracy"]["passed"] + quality_passed = results["response_quality"]["passed"] + + results["passed"] = tool_passed and quality_passed + + return results + + async def evaluate_all( + self, + responses: List[AgentResponse], + include_ai_eval: bool = True, + ) -> Dict[str, Any]: + """ + Evaluate all responses and generate summary statistics. + + Args: + responses: List of agent responses to evaluate + include_ai_eval: Whether to include AI-assisted evaluation + + Returns: + Dictionary with individual results and summary statistics + """ + individual_results = [] + + for i, response in enumerate(responses): + logger.info(f"Evaluating response {i+1}/{len(responses)}...") + result = await self.evaluate_response(response, include_ai_eval) + individual_results.append(result) + + # Compute summary statistics + total = len(individual_results) + passed = sum(1 for r in individual_results if r["passed"]) + + tool_f1_scores = [r["tool_accuracy"]["tool_f1_score"] for r in individual_results] + avg_tool_f1 = sum(tool_f1_scores) / total if total > 0 else 0 + + exec_times = [r["execution_time_ms"] for r in individual_results] + avg_exec_time = sum(exec_times) / total if total > 0 else 0 + + # Category breakdown + categories = {} + for result in individual_results: + cat = result["test_case"]["category"] + if cat not in categories: + categories[cat] = {"total": 0, "passed": 0} + categories[cat]["total"] += 1 + if result["passed"]: + categories[cat]["passed"] += 1 + + summary = { + "total_tests": total, + "passed": passed, + "failed": total - passed, + "pass_rate": passed / total if total > 0 else 0, + "average_tool_f1_score": avg_tool_f1, + "average_execution_time_ms": avg_exec_time, + "category_breakdown": categories, + "thresholds": { + "tool_call_accuracy": self.thresholds.tool_call_accuracy, + "groundedness": self.thresholds.groundedness, + "relevance": self.thresholds.relevance, + }, + } + + return { + "summary": summary, + "individual_results": individual_results, + "timestamp": datetime.utcnow().isoformat(), + } + + +def load_test_data(file_path: str) -> List[TestCase]: + """ + Load test cases from a JSONL file. + + Args: + file_path: Path to the JSONL file + + Returns: + List of TestCase objects + """ + test_cases = [] + + with open(file_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + if line: + data = json.loads(line) + test_cases.append(TestCase.from_dict(data)) + + return test_cases + + +def save_evaluation_results(results: Dict[str, Any], output_path: str) -> None: + """ + Save evaluation results to a JSON file. + + Args: + results: Evaluation results dictionary + output_path: Path to save the results + """ + with open(output_path, 'w', encoding='utf-8') as f: + json.dump(results, f, indent=2, default=str) + + logger.info(f"Evaluation results saved to {output_path}") + + +async def run_evaluation( + test_data_path: str, + agent_module: str = "agents.agent_framework.single_agent", + output_path: Optional[str] = None, + include_ai_eval: bool = True, +) -> Dict[str, Any]: + """ + Run a complete evaluation pipeline. + + Args: + test_data_path: Path to test data JSONL file + agent_module: Module path for the agent to test + output_path: Optional path to save results + include_ai_eval: Whether to include AI-assisted evaluation + + Returns: + Evaluation results dictionary + """ + logger.info(f"Loading test data from {test_data_path}") + test_cases = load_test_data(test_data_path) + logger.info(f"Loaded {len(test_cases)} test cases") + + # Run agent against test cases + logger.info(f"Running agent: {agent_module}") + runner = AgentRunner(agent_module=agent_module) + responses = await runner.run_test_dataset(test_cases) + logger.info(f"Collected {len(responses)} responses") + + # Evaluate responses + logger.info("Evaluating responses...") + evaluator = AgentEvaluator() + results = await evaluator.evaluate_all(responses, include_ai_eval=include_ai_eval) + + # Save results if output path provided + if output_path: + save_evaluation_results(results, output_path) + + # Print summary + summary = results["summary"] + print("\n" + "="*60) + print("EVALUATION SUMMARY") + print("="*60) + print(f"Total Tests: {summary['total_tests']}") + print(f"Passed: {summary['passed']}") + print(f"Failed: {summary['failed']}") + print(f"Pass Rate: {summary['pass_rate']:.1%}") + print(f"Avg Tool F1: {summary['average_tool_f1_score']:.2f}") + print(f"Avg Exec Time: {summary['average_execution_time_ms']:.0f}ms") + print("\nBy Category:") + for cat, stats in summary["category_breakdown"].items(): + rate = stats['passed'] / stats['total'] if stats['total'] > 0 else 0 + print(f" {cat}: {stats['passed']}/{stats['total']} ({rate:.0%})") + print("="*60 + "\n") + + return results + + +if __name__ == "__main__": + # Allow running as a standalone script + import argparse + + parser = argparse.ArgumentParser(description="Run agent evaluation") + parser.add_argument("--test-data", default="tests/evaluation/test_data.jsonl", + help="Path to test data JSONL file") + parser.add_argument("--agent-module", default="agents.agent_framework.single_agent", + help="Agent module to test") + parser.add_argument("--output", default=None, + help="Path to save evaluation results") + parser.add_argument("--no-ai-eval", action="store_true", + help="Disable AI-assisted evaluation") + + args = parser.parse_args() + + logging.basicConfig(level=logging.INFO) + + asyncio.run(run_evaluation( + test_data_path=args.test_data, + agent_module=args.agent_module, + output_path=args.output, + include_ai_eval=not args.no_ai_eval, + )) diff --git a/tests/evaluation/agent_runner.py b/tests/evaluation/agent_runner.py new file mode 100644 index 000000000..81efb1495 --- /dev/null +++ b/tests/evaluation/agent_runner.py @@ -0,0 +1,376 @@ +""" +Generic Agent Evaluation Runner + +This module provides a consistent interface for evaluating ANY agent implementation. +It works with any agent that follows the standard BaseAgent pattern: +1. Inherits from BaseAgent +2. Implements set_websocket_manager(manager) +3. Implements chat_async(prompt) -> str +4. Broadcasts events via _ws_manager.broadcast() + +Usage: + from agent_runner import AgentTestRunner, ToolCallTracker + + # Load any agent by module path + runner = AgentTestRunner("agents.agent_framework.single_agent") + + # Run with tool tracking + result = await runner.run_query("What is customer 251's balance?") + print(result.response) + print(result.tool_calls) +""" + +import asyncio +import importlib +import os +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from dotenv import load_dotenv + +# ═══════════════════════════════════════════════════════════════════════════════ +# PATH SETUP +# ═══════════════════════════════════════════════════════════════════════════════ + +_eval_dir = Path(__file__).parent.resolve() +_tests_dir = _eval_dir.parent +_workspace_root = _tests_dir.parent +_agentic_ai_dir = _workspace_root / "agentic_ai" + +sys.path.insert(0, str(_agentic_ai_dir)) +sys.path.insert(0, str(_tests_dir)) + +load_dotenv(_eval_dir / ".env") + + +# ═══════════════════════════════════════════════════════════════════════════════ +# TOOL CALL TRACKER +# ═══════════════════════════════════════════════════════════════════════════════ + + +class ToolCallTracker: + """ + Captures agent events by implementing the WebSocket manager interface. + + All agents in this codebase follow a consistent pattern: + 1. Inherit from BaseAgent + 2. Override set_websocket_manager(manager) to store the manager + 3. Call self._ws_manager.broadcast(session_id, message) for events + + This tracker captures those events, providing a consistent testing interface + for ANY agent implementation (current and future). + + Standard Event Types: + - tool_called: {type: "tool_called", tool_name: str, agent_id: str} + - agent_start: {type: "agent_start", agent_id: str, agent_name: str} + - agent_token: {type: "agent_token", agent_id: str, content: str} + - final_result: {type: "final_result", content: str} + """ + + def __init__(self): + self.events: list[dict] = [] + self.tool_calls: list[str] = [] + self.agent_transitions: list[str] = [] # For multi-agent tracking + + async def broadcast(self, session_id: str, message: dict) -> None: + """Capture broadcast messages (implements WebSocket manager interface).""" + self.events.append({"session_id": session_id, "timestamp": time.time(), **message}) + + msg_type = message.get("type", "") + + if msg_type == "tool_called": + tool_name = message.get("tool_name", "") + if tool_name: + self.tool_calls.append(tool_name) + + if msg_type == "agent_start": + agent_id = message.get("agent_id", "unknown") + self.agent_transitions.append(agent_id) + + def get_tool_calls(self) -> list[str]: + """Get list of tools called in order.""" + return self.tool_calls.copy() + + def get_unique_tools(self) -> set[str]: + """Get unique set of tools called.""" + return set(self.tool_calls) + + def get_agent_transitions(self) -> list[str]: + """Get agent transitions (for multi-agent evaluation).""" + return self.agent_transitions.copy() + + def get_events_by_type(self, event_type: str) -> list[dict]: + """Get all events of a specific type.""" + return [e for e in self.events if e.get("type") == event_type] + + def reset(self): + """Reset tracker for new conversation turn.""" + self.events.clear() + self.tool_calls.clear() + self.agent_transitions.clear() + + +# ═══════════════════════════════════════════════════════════════════════════════ +# QUERY RESULT +# ═══════════════════════════════════════════════════════════════════════════════ + + +@dataclass +class QueryResult: + """Result from running a query against an agent.""" + + query: str + response: str + tool_calls: list[str] = field(default_factory=list) + agent_transitions: list[str] = field(default_factory=list) + events: list[dict] = field(default_factory=list) + execution_time: float = 0.0 + error: str | None = None + + @property + def success(self) -> bool: + return self.error is None and bool(self.response) + + @property + def unique_tools(self) -> set[str]: + return set(self.tool_calls) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# AGENT TEST RUNNER +# ═══════════════════════════════════════════════════════════════════════════════ + + +class AgentTestRunner: + """ + Generic test runner for any agent implementation. + + Works with any agent that: + 1. Has an Agent class in the module + 2. Agent.__init__(state_store, session_id, ...) + 3. Agent.set_websocket_manager(manager) + 4. Agent.chat_async(prompt) -> str + + Example: + runner = AgentTestRunner("agents.agent_framework.single_agent") + result = await runner.run_query("Hello") + print(result.tool_calls) + """ + + # Known agent modules for convenience + KNOWN_AGENTS = { + "single": "agents.agent_framework.single_agent", + "reflection": "agents.agent_framework.multi_agent.reflection_agent", + "handoff": "agents.agent_framework.multi_agent.handoff_multi_domain_agent", + "magentic": "agents.agent_framework.multi_agent.magentic_group", + } + + def __init__(self, agent_module: str): + """ + Initialize runner with an agent module path. + + Args: + agent_module: Full module path (e.g., "agents.agent_framework.single_agent") + or shorthand ("single", "reflection", "handoff", "magentic") + """ + # Resolve shorthand names + self.agent_module = self.KNOWN_AGENTS.get(agent_module, agent_module) + self._agent_class = None + self._tracker = ToolCallTracker() + self._session_counter = 0 + + def _load_agent_class(self): + """Dynamically load the Agent class from the module.""" + if self._agent_class is None: + module = importlib.import_module(self.agent_module) + self._agent_class = getattr(module, "Agent") + return self._agent_class + + def _create_agent(self, session_id: str | None = None) -> Any: + """Create a new agent instance with fresh state.""" + AgentClass = self._load_agent_class() + + if session_id is None: + self._session_counter += 1 + session_id = f"eval_{self._session_counter}_{int(time.time() * 1000)}" + + state_store: dict[str, Any] = {} + agent = AgentClass(state_store=state_store, session_id=session_id) + agent.set_websocket_manager(self._tracker) + + return agent + + async def run_query( + self, + query: str, + session_id: str | None = None, + reset_tracker: bool = True, + ) -> QueryResult: + """ + Run a single query against the agent. + + Args: + query: User query to send + session_id: Optional session ID (auto-generated if not provided) + reset_tracker: Whether to reset the tracker before running + + Returns: + QueryResult with response, tool calls, and events + """ + if reset_tracker: + self._tracker.reset() + + agent = self._create_agent(session_id) + start_time = time.time() + + try: + response = await agent.chat_async(query) + + return QueryResult( + query=query, + response=response, + tool_calls=self._tracker.get_tool_calls(), + agent_transitions=self._tracker.get_agent_transitions(), + events=self._tracker.events.copy(), + execution_time=time.time() - start_time, + ) + except Exception as e: + return QueryResult( + query=query, + response="", + tool_calls=self._tracker.get_tool_calls(), + agent_transitions=self._tracker.get_agent_transitions(), + events=self._tracker.events.copy(), + execution_time=time.time() - start_time, + error=str(e), + ) + + async def run_conversation( + self, + queries: list[str], + session_id: str | None = None, + ) -> list[QueryResult]: + """ + Run a multi-turn conversation with the same agent instance. + + Args: + queries: List of user queries in order + session_id: Session ID for conversation continuity + + Returns: + List of QueryResult, one per turn + """ + if session_id is None: + self._session_counter += 1 + session_id = f"conv_{self._session_counter}_{int(time.time() * 1000)}" + + agent = self._create_agent(session_id) + results = [] + + for query in queries: + self._tracker.reset() # Reset per turn + start_time = time.time() + + try: + response = await agent.chat_async(query) + + results.append(QueryResult( + query=query, + response=response, + tool_calls=self._tracker.get_tool_calls(), + agent_transitions=self._tracker.get_agent_transitions(), + events=self._tracker.events.copy(), + execution_time=time.time() - start_time, + )) + except Exception as e: + results.append(QueryResult( + query=query, + response="", + tool_calls=self._tracker.get_tool_calls(), + agent_transitions=self._tracker.get_agent_transitions(), + events=self._tracker.events.copy(), + execution_time=time.time() - start_time, + error=str(e), + )) + + return results + + +# ═══════════════════════════════════════════════════════════════════════════════ +# CONVENIENCE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════════ + + +def list_available_agents() -> dict[str, str]: + """List known agent shorthand names and their module paths.""" + return AgentTestRunner.KNOWN_AGENTS.copy() + + +async def compare_agents( + query: str, + agent_modules: list[str] | None = None, +) -> dict[str, QueryResult]: + """ + Run the same query against multiple agents and compare results. + + Args: + query: Query to run + agent_modules: List of agent modules (defaults to all known agents) + + Returns: + Dict mapping agent name to QueryResult + """ + if agent_modules is None: + agent_modules = ["single", "reflection"] + + results = {} + for agent_name in agent_modules: + runner = AgentTestRunner(agent_name) + result = await runner.run_query(query) + results[agent_name] = result + + status = "✅" if result.success else "❌" + print(f"{status} {agent_name}: {result.execution_time:.1f}s, " + f"tools={len(result.tool_calls)}") + + return results + + +# ═══════════════════════════════════════════════════════════════════════════════ +# STANDALONE DEMO +# ═══════════════════════════════════════════════════════════════════════════════ + +if __name__ == "__main__": + import logging + import warnings + + # Suppress MCP client cleanup warnings (they don't affect results) + logging.getLogger("asyncio").setLevel(logging.CRITICAL) + warnings.filterwarnings("ignore", category=DeprecationWarning) + + async def demo(): + print("Agent Test Runner Demo") + print("=" * 50) + + # Show available agents + print("\nAvailable agents:") + for name, module in list_available_agents().items(): + print(f" {name}: {module}") + + # Run a simple comparison + print("\nComparing agents on a simple query...") + query = "Hi, I'm customer 251. Can you tell me about my account?" + + results = await compare_agents(query, ["single", "reflection"]) + + print("\n" + "-" * 50) + for name, result in results.items(): + print(f"\n{name}:") + print(f" Response: {result.response[:150]}...") + print(f" Tools: {result.tool_calls}") + print(f" Time: {result.execution_time:.1f}s") + + asyncio.run(demo()) diff --git a/tests/evaluation/all_agents_comparison.json b/tests/evaluation/all_agents_comparison.json new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/tests/evaluation/all_agents_comparison.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/evaluation/llm_judge_evaluator.py b/tests/evaluation/llm_judge_evaluator.py new file mode 100644 index 000000000..522f81691 --- /dev/null +++ b/tests/evaluation/llm_judge_evaluator.py @@ -0,0 +1,759 @@ +""" +LLM-as-Judge Evaluator using Azure AI Foundry Evaluation SDK. + +This module provides LLM-based evaluation for AI agents, replacing simple +keyword matching with sophisticated AI-assisted judgment. + +Azure AI Foundry Evaluators Used: +--------------------------------- +AGENT-SPECIFIC (Process Evaluation): +- IntentResolutionEvaluator: Did the agent correctly identify user intent? +- TaskAdherenceEvaluator: Did the response follow the assigned task/system prompt? +- ToolCallAccuracyEvaluator: Were the correct tools called with proper arguments? + +QUALITY METRICS (System Evaluation): +- CoherenceEvaluator: Is the response logically coherent? +- FluencyEvaluator: Is the response well-written? +- RelevanceEvaluator: Is the response relevant to the query? +- ResponseCompletenessEvaluator: Does the response fully address the query? + +MULTI-TURN SUPPORT: +- Azure AI Foundry supports conversation format with messages list +- Each message has role (system/user/assistant/tool), content, and optional tool_calls +- Evaluators understand conversation context and tool interactions + +Reference: https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/develop/agent-evaluate-sdk +""" + +import os +import json +import asyncio +from pathlib import Path +from dataclasses import dataclass, field +from typing import Optional +from datetime import datetime + +from dotenv import load_dotenv + +# Load environment from evaluation folder +_eval_dir = Path(__file__).parent +load_dotenv(_eval_dir / ".env") + +# Azure AI Evaluation imports +try: + from azure.ai.evaluation import ( + IntentResolutionEvaluator, + TaskAdherenceEvaluator, + ToolCallAccuracyEvaluator, + CoherenceEvaluator, + FluencyEvaluator, + RelevanceEvaluator, + # ResponseCompletenessEvaluator, # May not be available in all versions + ) + EVALUATORS_AVAILABLE = True +except ImportError as e: + print(f"Warning: Azure AI Evaluation SDK not fully available: {e}") + EVALUATORS_AVAILABLE = False + + +# ═══════════════════════════════════════════════════════════════════════════════ +# DATA STRUCTURES +# ═══════════════════════════════════════════════════════════════════════════════ + + +@dataclass +class ToolCall: + """Represents a tool call made by the agent.""" + name: str + arguments: dict = field(default_factory=dict) + tool_call_id: str = "" + result: Optional[dict] = None + + +@dataclass +class ConversationMessage: + """A message in the conversation (OpenAI-style format).""" + role: str # "system", "user", "assistant", "tool" + content: str + tool_calls: list[ToolCall] = field(default_factory=list) + tool_call_id: Optional[str] = None # For tool response messages + timestamp: Optional[str] = None + + +@dataclass +class ToolDefinition: + """Definition of a tool available to the agent.""" + name: str + description: str + parameters: dict = field(default_factory=dict) + + +@dataclass +class EvaluationInput: + """Input data for LLM-judge evaluation.""" + query: str # User query or conversation history + response: str # Agent's final response + tool_calls: list[ToolCall] = field(default_factory=list) + tool_definitions: list[ToolDefinition] = field(default_factory=list) + system_prompt: str = "" # Agent's system prompt for TaskAdherence + conversation: list[ConversationMessage] = field(default_factory=list) + + +@dataclass +class EvaluationResult: + """Result from LLM-judge evaluation.""" + # Intent Resolution + intent_resolution_score: Optional[float] = None + intent_resolution_result: Optional[str] = None # "pass" or "fail" + intent_resolution_reason: Optional[str] = None + + # Task Adherence + task_adherence_score: Optional[float] = None + task_adherence_result: Optional[str] = None + task_adherence_reason: Optional[str] = None + + # Tool Call Accuracy + tool_call_accuracy_score: Optional[float] = None + tool_call_accuracy_result: Optional[str] = None + tool_call_accuracy_reason: Optional[str] = None + + # Quality Metrics + coherence_score: Optional[float] = None + fluency_score: Optional[float] = None + relevance_score: Optional[float] = None + + # Solution Accuracy (custom evaluator using ground truth + rubric) + solution_accuracy_score: Optional[float] = None + solution_accuracy_reason: Optional[str] = None + + # Overall + overall_pass: bool = False + evaluation_time: float = 0.0 + errors: list[str] = field(default_factory=list) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# MODEL CONFIGURATION +# ═══════════════════════════════════════════════════════════════════════════════ + + +def get_model_config() -> dict: + """Get Azure OpenAI model configuration for evaluators.""" + return { + "azure_endpoint": os.getenv("AZURE_OPENAI_ENDPOINT"), + "api_key": os.getenv("AZURE_OPENAI_KEY"), + "api_version": os.getenv("AZURE_OPENAI_API_VERSION", "2024-08-01-preview"), + "azure_deployment": os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o"), + } + + +def _safe_float(value: any) -> Optional[float]: + """Safely convert SDK output to float, handling string values.""" + if value is None: + return None + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + try: + return float(value) + except ValueError: + # Could be "pass" or "fail" - not a numeric score + return None + return None + + +# ═══════════════════════════════════════════════════════════════════════════════ +# SOLUTION ACCURACY EVALUATOR (Custom LLM-as-Judge) +# ═══════════════════════════════════════════════════════════════════════════════ + +SOLUTION_ACCURACY_PROMPT = """You are an expert evaluator assessing how well an AI agent's response addresses a customer service scenario. + +## Ground Truth Solution +This is the ideal/expected solution for the scenario: +{ground_truth} + +## Scoring Rubric +{scoring_rubric} + +## Agent's Response +{agent_response} + +## Your Task +Compare the agent's response against the ground truth solution using the scoring rubric. +Consider: +1. Does the response correctly identify the root cause/issue? +2. Does it provide accurate information (numbers, facts, policies)? +3. Does it offer appropriate solutions or next steps? +4. Does it address the customer's actual needs? + +Provide your evaluation in this exact format: +SCORE: [1-5] +REASON: [One paragraph explaining why you gave this score, referencing specific parts of the rubric] +""" + + +class SolutionAccuracyEvaluator: + """ + Custom evaluator that scores agent responses against ground truth solutions + using a scoring rubric. This provides domain-specific accuracy evaluation. + """ + + def __init__(self, model_config: Optional[dict] = None): + self.model_config = model_config or get_model_config() + self._client = None + + def _get_client(self): + """Lazily initialize the Azure OpenAI client.""" + if self._client is None: + try: + from openai import AzureOpenAI + self._client = AzureOpenAI( + azure_endpoint=self.model_config["azure_endpoint"], + api_key=self.model_config["api_key"], + api_version=self.model_config["api_version"], + ) + except Exception as e: + print(f"Failed to initialize OpenAI client: {e}") + return self._client + + async def evaluate( + self, + agent_response: str, + ground_truth: str, + scoring_rubric: str, + ) -> tuple[Optional[float], Optional[str]]: + """ + Evaluate agent response against ground truth using the rubric. + + Returns: + (score, reason) tuple where score is 1-5 or None on error + """ + if not ground_truth or not scoring_rubric: + return None, "No ground truth or rubric provided" + + client = self._get_client() + if not client: + return None, "OpenAI client not available" + + prompt = SOLUTION_ACCURACY_PROMPT.format( + ground_truth=ground_truth, + scoring_rubric=scoring_rubric, + agent_response=agent_response, + ) + + try: + loop = asyncio.get_event_loop() + response = await loop.run_in_executor( + None, + lambda: client.chat.completions.create( + model=self.model_config["azure_deployment"], + messages=[{"role": "user", "content": prompt}], + temperature=0.0, + max_tokens=500, + ) + ) + + content = response.choices[0].message.content + + # Parse response + score = None + reason = None + + for line in content.split("\n"): + if line.startswith("SCORE:"): + try: + score = float(line.replace("SCORE:", "").strip()) + except ValueError: + pass + elif line.startswith("REASON:"): + reason = line.replace("REASON:", "").strip() + + # If reason spans multiple lines, get the rest + if "REASON:" in content: + reason_start = content.find("REASON:") + len("REASON:") + reason = content[reason_start:].strip() + + return score, reason + + except Exception as e: + return None, f"Evaluation error: {e}" + + +# ═══════════════════════════════════════════════════════════════════════════════ +# LLM JUDGE EVALUATOR +# ═══════════════════════════════════════════════════════════════════════════════ + + +class LLMJudgeEvaluator: + """ + Evaluates agents using Azure AI Foundry's LLM-as-judge evaluators. + + This replaces simple keyword matching with sophisticated AI-assisted judgment + that can understand context, intent, and quality of responses. + + Example usage: + evaluator = LLMJudgeEvaluator() + + result = await evaluator.evaluate( + query="What's my invoice total?", + response="Your invoice total is $150.00", + tool_calls=[ToolCall(name="get_customer_invoices", arguments={"customer_id": 1})], + tool_definitions=[ToolDefinition( + name="get_customer_invoices", + description="Get invoices for a customer" + )] + ) + + print(f"Intent Resolution: {result.intent_resolution_result}") + print(f"Tool Accuracy: {result.tool_call_accuracy_result}") + """ + + def __init__( + self, + model_config: Optional[dict] = None, + use_reasoning_model: bool = False, # Set True for o-series models + enable_agent_evaluators: bool = True, + enable_quality_evaluators: bool = True, + ): + """ + Initialize LLM Judge evaluator. + + Args: + model_config: Azure OpenAI configuration (uses env vars if None) + use_reasoning_model: Set True if using o-series reasoning models + enable_agent_evaluators: Enable IntentResolution, TaskAdherence, ToolCallAccuracy + enable_quality_evaluators: Enable Coherence, Fluency, Relevance + """ + self.model_config = model_config or get_model_config() + self.use_reasoning_model = use_reasoning_model + self.enable_agent_evaluators = enable_agent_evaluators + self.enable_quality_evaluators = enable_quality_evaluators + + self._evaluators: dict = {} + self._initialized = False + + # Custom solution accuracy evaluator (always available) + self._solution_evaluator = SolutionAccuracyEvaluator(self.model_config) + + def _init_evaluators(self): + """Lazily initialize evaluators.""" + if self._initialized or not EVALUATORS_AVAILABLE: + return + + try: + if self.enable_agent_evaluators: + # Agent-specific evaluators (support reasoning models) + eval_kwargs = {"model_config": self.model_config} + if self.use_reasoning_model: + eval_kwargs["is_reasoning_model"] = True + + self._evaluators["intent_resolution"] = IntentResolutionEvaluator(**eval_kwargs) + self._evaluators["task_adherence"] = TaskAdherenceEvaluator(**eval_kwargs) + self._evaluators["tool_call_accuracy"] = ToolCallAccuracyEvaluator(**eval_kwargs) + + if self.enable_quality_evaluators: + # Quality evaluators (don't use reasoning model for efficiency) + quality_config = {"model_config": self.model_config} + self._evaluators["coherence"] = CoherenceEvaluator(**quality_config) + self._evaluators["fluency"] = FluencyEvaluator(**quality_config) + self._evaluators["relevance"] = RelevanceEvaluator(**quality_config) + + self._initialized = True + print(f"✅ Initialized {len(self._evaluators)} LLM-judge evaluators") + + except Exception as e: + print(f"⚠️ Error initializing evaluators: {e}") + self._initialized = True # Don't retry + + def _format_tool_calls(self, tool_calls: list[ToolCall]) -> list[dict]: + """Format tool calls for Azure AI Evaluation SDK.""" + return [ + { + "type": "tool_call", + "tool_call_id": tc.tool_call_id or f"call_{i}", + "name": tc.name, + "arguments": tc.arguments + } + for i, tc in enumerate(tool_calls) + ] + + def _format_tool_definitions(self, tool_definitions: list[ToolDefinition]) -> list[dict]: + """Format tool definitions for Azure AI Evaluation SDK.""" + return [ + { + "name": td.name, + "description": td.description, + "parameters": td.parameters or { + "type": "object", + "properties": {}, + } + } + for td in tool_definitions + ] + + def _format_conversation_query( + self, + query: str, + system_prompt: str, + conversation: list[ConversationMessage] + ) -> list[dict]: + """ + Format conversation history as query for multi-turn evaluation. + + Azure AI Foundry expects query as a list of OpenAI-style messages + for multi-turn evaluation. + """ + messages = [] + + # System message first (required) + if system_prompt: + messages.append({ + "role": "system", + "content": system_prompt + }) + else: + messages.append({ + "role": "system", + "content": "You are a helpful customer service agent." + }) + + # Add conversation history + for msg in conversation: + message = { + "role": msg.role, + "createdAt": msg.timestamp or datetime.now().isoformat() + "Z", + } + + if msg.role == "tool": + message["content"] = [{"type": "tool_result", "tool_result": msg.content}] + if msg.tool_call_id: + message["tool_call_id"] = msg.tool_call_id + elif msg.tool_calls: + message["content"] = [ + { + "type": "tool_call", + "tool_call_id": tc.tool_call_id or f"call_{i}", + "name": tc.name, + "arguments": tc.arguments + } + for i, tc in enumerate(msg.tool_calls) + ] + else: + message["content"] = [{"type": "text", "text": msg.content}] + + messages.append(message) + + # Final user query if not already in conversation + if not conversation or conversation[-1].role != "user": + messages.append({ + "role": "user", + "createdAt": datetime.now().isoformat() + "Z", + "content": [{"type": "text", "text": query}] + }) + + return messages + + async def evaluate( + self, + query: str, + response: str, + tool_calls: Optional[list[ToolCall]] = None, + tool_definitions: Optional[list[ToolDefinition]] = None, + system_prompt: str = "", + conversation: Optional[list[ConversationMessage]] = None, + ground_truth_solution: str = "", + scoring_rubric: str = "", + ) -> EvaluationResult: + """ + Evaluate agent response using LLM judges. + + Args: + query: The user's query + response: The agent's response + tool_calls: List of tools the agent called + tool_definitions: Available tool definitions + system_prompt: Agent's system prompt + conversation: Full conversation history for multi-turn + ground_truth_solution: The ideal/expected solution for the scenario + scoring_rubric: Criteria for evaluating solution accuracy (1-5 scale) + + Returns: + EvaluationResult with scores, pass/fail, and reasons + """ + import time + start_time = time.time() + + result = EvaluationResult() + + if not EVALUATORS_AVAILABLE: + result.errors.append("Azure AI Evaluation SDK not available") + return result + + self._init_evaluators() + + # Format inputs + formatted_tool_calls = self._format_tool_calls(tool_calls or []) + formatted_tool_defs = self._format_tool_definitions(tool_definitions or []) + + # Use conversation format for multi-turn if provided + if conversation: + formatted_query = self._format_conversation_query( + query, system_prompt, conversation + ) + else: + formatted_query = query + + # Run evaluators (they're synchronous, so we run in executor) + loop = asyncio.get_event_loop() + + # Intent Resolution + if "intent_resolution" in self._evaluators: + try: + intent_result = await loop.run_in_executor( + None, + lambda: self._evaluators["intent_resolution"]( + query=formatted_query, + response=response, + ) + ) + result.intent_resolution_score = _safe_float(intent_result.get("intent_resolution")) + result.intent_resolution_result = intent_result.get("intent_resolution_result") + result.intent_resolution_reason = intent_result.get("intent_resolution_reason") + except Exception as e: + result.errors.append(f"IntentResolution error: {e}") + + # Task Adherence + if "task_adherence" in self._evaluators: + try: + task_result = await loop.run_in_executor( + None, + lambda: self._evaluators["task_adherence"]( + query=formatted_query, + response=response, + ) + ) + result.task_adherence_score = _safe_float(task_result.get("task_adherence")) + result.task_adherence_result = task_result.get("task_adherence_result") + result.task_adherence_reason = task_result.get("task_adherence_reason") + except Exception as e: + result.errors.append(f"TaskAdherence error: {e}") + + # Tool Call Accuracy (only if tool_calls provided) + if "tool_call_accuracy" in self._evaluators and formatted_tool_calls: + try: + tool_result = await loop.run_in_executor( + None, + lambda: self._evaluators["tool_call_accuracy"]( + query=query, # Simple string for tool accuracy + tool_calls=formatted_tool_calls, + tool_definitions=formatted_tool_defs, + ) + ) + result.tool_call_accuracy_score = _safe_float(tool_result.get("tool_call_accuracy")) + result.tool_call_accuracy_result = tool_result.get("tool_call_accuracy_result") + result.tool_call_accuracy_reason = str(tool_result.get("details", "")) + except Exception as e: + result.errors.append(f"ToolCallAccuracy error: {e}") + + # Quality Metrics + if "coherence" in self._evaluators: + try: + coh_result = await loop.run_in_executor( + None, + lambda: self._evaluators["coherence"]( + query=query, + response=response, + ) + ) + result.coherence_score = _safe_float(coh_result.get("coherence")) + except Exception as e: + result.errors.append(f"Coherence error: {e}") + + if "fluency" in self._evaluators: + try: + flu_result = await loop.run_in_executor( + None, + lambda: self._evaluators["fluency"]( + query=query, + response=response, + ) + ) + result.fluency_score = _safe_float(flu_result.get("fluency")) + except Exception as e: + result.errors.append(f"Fluency error: {e}") + + if "relevance" in self._evaluators: + try: + rel_result = await loop.run_in_executor( + None, + lambda: self._evaluators["relevance"]( + query=query, + response=response, + ) + ) + result.relevance_score = _safe_float(rel_result.get("relevance")) + except Exception as e: + result.errors.append(f"Relevance error: {e}") + + # Solution Accuracy (custom evaluator with ground truth + rubric) + if ground_truth_solution and scoring_rubric: + try: + score, reason = await self._solution_evaluator.evaluate( + agent_response=response, + ground_truth=ground_truth_solution, + scoring_rubric=scoring_rubric, + ) + result.solution_accuracy_score = score + result.solution_accuracy_reason = reason + except Exception as e: + result.errors.append(f"SolutionAccuracy error: {e}") + + # Determine overall pass + passes = [] + if result.intent_resolution_result: + passes.append(result.intent_resolution_result == "pass") + if result.task_adherence_result: + passes.append(result.task_adherence_result == "pass") + if result.tool_call_accuracy_result: + passes.append(result.tool_call_accuracy_result == "pass") + # Solution accuracy: pass if score >= 3 (Adequate or better) + if result.solution_accuracy_score is not None: + passes.append(result.solution_accuracy_score >= 3) + + result.overall_pass = all(passes) if passes else False + result.evaluation_time = time.time() - start_time + + return result + + def evaluate_sync(self, **kwargs) -> EvaluationResult: + """Synchronous wrapper for evaluate().""" + return asyncio.run(self.evaluate(**kwargs)) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# CONVENIENCE FUNCTIONS +# ═══════════════════════════════════════════════════════════════════════════════ + + +async def evaluate_agent_response( + query: str, + response: str, + tool_calls: Optional[list[str]] = None, # Just tool names for simplicity + tool_definitions: Optional[list[dict]] = None, +) -> EvaluationResult: + """ + Simple function to evaluate an agent response. + + Args: + query: User's query + response: Agent's response + tool_calls: List of tool names that were called + tool_definitions: List of {name, description} dicts + + Returns: + EvaluationResult with scores and pass/fail + """ + evaluator = LLMJudgeEvaluator() + + # Convert simple tool names to ToolCall objects + tc_objects = [ToolCall(name=name) for name in (tool_calls or [])] + + # Convert simple dicts to ToolDefinition objects + td_objects = [ + ToolDefinition( + name=td.get("name", ""), + description=td.get("description", "") + ) + for td in (tool_definitions or []) + ] + + return await evaluator.evaluate( + query=query, + response=response, + tool_calls=tc_objects, + tool_definitions=td_objects, + ) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# DEMO +# ═══════════════════════════════════════════════════════════════════════════════ + + +async def demo(): + """Demo the LLM judge evaluator.""" + print("=" * 80) + print("LLM-as-Judge Evaluator Demo") + print("=" * 80) + + if not EVALUATORS_AVAILABLE: + print("\n❌ Azure AI Evaluation SDK not available.") + print("Install with: pip install azure-ai-evaluation") + return + + # Check required environment variables + required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_KEY"] + missing = [v for v in required_vars if not os.getenv(v)] + if missing: + print(f"\n⚠️ Missing environment variables: {missing}") + print("Set these in tests/evaluation/.env") + return + + evaluator = LLMJudgeEvaluator( + enable_agent_evaluators=True, + enable_quality_evaluators=True, + ) + + # Test case: Customer asks about invoice + print("\n📋 Test Case: Invoice Query") + print("-" * 40) + + result = await evaluator.evaluate( + query="What is my current invoice total for account 123?", + response="Based on your account records, your current invoice total is $542.50. This includes your monthly subscription fee of $99.99, data overage charges of $42.51, and equipment rental of $400.00.", + tool_calls=[ + ToolCall( + name="get_customer_invoices", + arguments={"customer_id": 123} + ) + ], + tool_definitions=[ + ToolDefinition( + name="get_customer_invoices", + description="Retrieves invoice details for a customer account" + ), + ToolDefinition( + name="get_customer_detail", + description="Gets customer profile information" + ), + ], + ) + + print(f"\n🎯 Intent Resolution:") + print(f" Score: {result.intent_resolution_score}/5") + print(f" Result: {result.intent_resolution_result}") + print(f" Reason: {result.intent_resolution_reason}") + + print(f"\n📋 Task Adherence:") + print(f" Score: {result.task_adherence_score}/5") + print(f" Result: {result.task_adherence_result}") + print(f" Reason: {result.task_adherence_reason}") + + print(f"\n🔧 Tool Call Accuracy:") + print(f" Score: {result.tool_call_accuracy_score}/5") + print(f" Result: {result.tool_call_accuracy_result}") + + print(f"\n✨ Quality Metrics:") + print(f" Coherence: {result.coherence_score}/5") + print(f" Fluency: {result.fluency_score}/5") + print(f" Relevance: {result.relevance_score}/5") + + print(f"\n{'✅ OVERALL PASS' if result.overall_pass else '❌ OVERALL FAIL'}") + print(f"⏱️ Evaluation time: {result.evaluation_time:.2f}s") + + if result.errors: + print(f"\n⚠️ Errors: {result.errors}") + + +if __name__ == "__main__": + asyncio.run(demo()) diff --git a/tests/evaluation/pyproject.toml b/tests/evaluation/pyproject.toml new file mode 100644 index 000000000..fea5a82c0 --- /dev/null +++ b/tests/evaluation/pyproject.toml @@ -0,0 +1,42 @@ +[project] +name = "agent-evaluation" +version = "0.1.0" +description = "Agent evaluation framework for Contoso AI Workshop" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + # Agent Framework (same as applications) + "agent-framework==1.0.0b260107", + + # Azure OpenAI + "openai>=2.5.0", + "azure-identity>=1.15.0", + + # Testing + "pytest>=8.0.0", + "pytest-asyncio>=0.23.0", + "pytest-timeout>=2.3.0", + + # Evaluation SDK (optional - for AI-assisted metrics) + "azure-ai-evaluation>=1.0.0", + + # Utilities + "python-dotenv>=1.0.0", + "httpx>=0.27.0", + "pydantic>=2.0.0", +] + +[tool.uv] +prerelease = "allow" + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["."] +python_files = ["test_*.py", "*_test.py"] +markers = [ + "unit: Unit tests (no external dependencies)", + "integration: Integration tests (require MCP + Azure OpenAI)", + "evaluation: Agent evaluation tests", + "slow: Slow tests (full evaluation pipeline)", + "comparison: Agent comparison tests", +] diff --git a/tests/evaluation/test_agent_comparison.py b/tests/evaluation/test_agent_comparison.py new file mode 100644 index 000000000..f9deb4dae --- /dev/null +++ b/tests/evaluation/test_agent_comparison.py @@ -0,0 +1,484 @@ +""" +Agent Comparison Tests + +This module compares the performance of different agent implementations +(single_agent vs reflection_agent) using the same test dataset. + +Usage: + # Run from tests/evaluation folder: + uv run pytest test_agent_comparison.py -v + + # Run with detailed comparison report: + uv run pytest test_agent_comparison.py -v -s --tb=short + + # Run quick comparison (fewer test cases): + uv run pytest test_agent_comparison.py -v -k "quick" +""" + +import asyncio +import json +import os +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import pytest +from dotenv import load_dotenv + +# ═══════════════════════════════════════════════════════════════════════════════ +# PATH SETUP +# ═══════════════════════════════════════════════════════════════════════════════ + +# Add paths for imports +_eval_dir = Path(__file__).parent.resolve() +_tests_dir = _eval_dir.parent +_workspace_root = _tests_dir.parent +_agentic_ai_dir = _workspace_root / "agentic_ai" + +# Add paths for agent imports +sys.path.insert(0, str(_agentic_ai_dir)) +sys.path.insert(0, str(_tests_dir)) + +# Load environment from evaluation folder +load_dotenv(_eval_dir / ".env") + +# ═══════════════════════════════════════════════════════════════════════════════ +# DATA CLASSES +# ═══════════════════════════════════════════════════════════════════════════════ + + +@dataclass +class AgentResult: + """Result from a single agent run.""" + + agent_name: str + query: str + response: str + tools_called: list[str] + execution_time: float + error: str | None = None + + @property + def success(self) -> bool: + return self.error is None and bool(self.response) + + +@dataclass +class ComparisonMetrics: + """Comparison metrics between two agents.""" + + agent_a: str + agent_b: str + test_count: int + agent_a_metrics: dict[str, float] = field(default_factory=dict) + agent_b_metrics: dict[str, float] = field(default_factory=dict) + differences: dict[str, float] = field(default_factory=dict) + + def to_report(self) -> str: + """Generate a formatted comparison report.""" + lines = [ + "═" * 70, + f"AGENT COMPARISON REPORT: {self.agent_a} vs {self.agent_b}", + "═" * 70, + f"Test Cases: {self.test_count}", + "", + f"{'Metric':<30} {self.agent_a:>15} {self.agent_b:>15} {'Diff':>10}", + "-" * 70, + ] + + all_metrics = set(self.agent_a_metrics.keys()) | set(self.agent_b_metrics.keys()) + for metric in sorted(all_metrics): + a_val = self.agent_a_metrics.get(metric, 0) + b_val = self.agent_b_metrics.get(metric, 0) + diff = self.differences.get(metric, b_val - a_val) + diff_str = f"+{diff:.3f}" if diff > 0 else f"{diff:.3f}" + lines.append(f"{metric:<30} {a_val:>15.3f} {b_val:>15.3f} {diff_str:>10}") + + lines.extend(["-" * 70, ""]) + return "\n".join(lines) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# AGENT RUNNER +# ═══════════════════════════════════════════════════════════════════════════════ + + +class AgentComparisonRunner: + """Runs and compares multiple agent implementations.""" + + def __init__(self): + self.mcp_server_uri = os.getenv("MCP_SERVER_URI", "http://localhost:8000/mcp") + self.azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") + self.azure_api_key = os.getenv("AZURE_OPENAI_API_KEY") + self.deployment = os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT") + self.api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2025-03-01-preview") + + # Agent module paths + self.single_agent_module = os.getenv( + "SINGLE_AGENT_MODULE", + "agents.agent_framework.single_agent" + ) + self.reflection_agent_module = os.getenv( + "REFLECTION_AGENT_MODULE", + "agents.agent_framework.multi_agent.reflection_agent" + ) + + async def _run_single_agent(self, query: str, session_id: str | None = None) -> AgentResult: + """Run the single agent implementation.""" + from agents.agent_framework.single_agent import Agent + + start_time = time.time() + tools_called = [] + error = None + response = "" + + # Use unique session ID for isolation + if session_id is None: + session_id = f"eval_single_{int(time.time() * 1000)}" + + state_store: dict[str, Any] = {} + + try: + # Create agent instance + agent = Agent(state_store=state_store, session_id=session_id) + + # Run the agent + response = await agent.chat_async(query) + + # Try to extract tool calls from chat history if available + if hasattr(agent, 'chat_history'): + for entry in agent.chat_history: + if isinstance(entry, dict) and 'tool_calls' in entry: + for tc in entry.get('tool_calls', []): + if isinstance(tc, dict): + tools_called.append(tc.get('name', str(tc))) + else: + tools_called.append(str(tc)) + + except Exception as e: + error = str(e) + response = "" + + return AgentResult( + agent_name="single_agent", + query=query, + response=response, + tools_called=tools_called, + execution_time=time.time() - start_time, + error=error + ) + + async def _run_reflection_agent(self, query: str, session_id: str | None = None) -> AgentResult: + """Run the reflection agent implementation.""" + from agents.agent_framework.multi_agent.reflection_agent import Agent + + start_time = time.time() + tools_called = [] + error = None + response = "" + + # Use unique session ID for isolation + if session_id is None: + session_id = f"eval_reflection_{int(time.time() * 1000)}" + + state_store: dict[str, Any] = {} + + try: + # Create agent instance + agent = Agent(state_store=state_store, session_id=session_id) + + # Run the agent + response = await agent.chat_async(query) + + # Try to extract tool calls from chat history if available + if hasattr(agent, 'chat_history'): + for entry in agent.chat_history: + if isinstance(entry, dict) and 'tool_calls' in entry: + for tc in entry.get('tool_calls', []): + if isinstance(tc, dict): + tools_called.append(tc.get('name', str(tc))) + else: + tools_called.append(str(tc)) + + except Exception as e: + error = str(e) + response = "" + + return AgentResult( + agent_name="reflection_agent", + query=query, + response=response, + tools_called=tools_called, + execution_time=time.time() - start_time, + error=error + ) + + async def run_comparison( + self, + queries: list[str], + expected_tools: list[list[str]] | None = None + ) -> ComparisonMetrics: + """Run both agents on a list of queries and compare results.""" + + single_results: list[AgentResult] = [] + reflection_results: list[AgentResult] = [] + + for i, query in enumerate(queries): + print(f"\n[{i+1}/{len(queries)}] Testing: {query[:50]}...") + + # Run both agents + single_result = await self._run_single_agent(query) + single_results.append(single_result) + print(f" Single Agent: {single_result.execution_time:.2f}s, " + f"tools={len(single_result.tools_called)}, " + f"success={single_result.success}") + + reflection_result = await self._run_reflection_agent(query) + reflection_results.append(reflection_result) + print(f" Reflection Agent: {reflection_result.execution_time:.2f}s, " + f"tools={len(reflection_result.tools_called)}, " + f"success={reflection_result.success}") + + # Calculate metrics + metrics = ComparisonMetrics( + agent_a="single_agent", + agent_b="reflection_agent", + test_count=len(queries) + ) + + # Calculate single agent metrics + metrics.agent_a_metrics = self._calculate_metrics(single_results, expected_tools) + + # Calculate reflection agent metrics + metrics.agent_b_metrics = self._calculate_metrics(reflection_results, expected_tools) + + # Calculate differences (reflection - single) + for key in metrics.agent_a_metrics: + metrics.differences[key] = ( + metrics.agent_b_metrics.get(key, 0) - + metrics.agent_a_metrics.get(key, 0) + ) + + return metrics + + def _calculate_metrics( + self, + results: list[AgentResult], + expected_tools: list[list[str]] | None = None + ) -> dict[str, float]: + """Calculate aggregate metrics from results.""" + if not results: + return {} + + metrics = {} + + # Success rate + success_count = sum(1 for r in results if r.success) + metrics["success_rate"] = success_count / len(results) + + # Average execution time + metrics["avg_execution_time"] = sum(r.execution_time for r in results) / len(results) + + # Average response length + metrics["avg_response_length"] = sum(len(r.response) for r in results) / len(results) + + # Average tools called + metrics["avg_tools_called"] = sum(len(r.tools_called) for r in results) / len(results) + + # Tool accuracy (if expected tools provided) + if expected_tools and len(expected_tools) == len(results): + tool_accuracies = [] + for result, expected in zip(results, expected_tools): + if not expected: + continue + called_set = set(result.tools_called) + expected_set = set(expected) + if expected_set: + accuracy = len(called_set & expected_set) / len(expected_set) + tool_accuracies.append(accuracy) + + if tool_accuracies: + metrics["tool_accuracy"] = sum(tool_accuracies) / len(tool_accuracies) + + return metrics + + +# ═══════════════════════════════════════════════════════════════════════════════ +# TEST DATA LOADING +# ═══════════════════════════════════════════════════════════════════════════════ + + +def load_test_data(count: int | None = None) -> tuple[list[str], list[list[str]], list[str]]: + """Load test data from test_data.jsonl. + + Returns: + Tuple of (queries, expected_tools, ground_truths) + """ + test_data_file = _eval_dir / "test_data.jsonl" + queries = [] + expected_tools = [] + ground_truths = [] + + with open(test_data_file, "r") as f: + for line in f: + if not line.strip(): + continue + data = json.loads(line) + queries.append(data["query"]) + expected_tools.append(data.get("expected_tools", [])) + ground_truths.append(data.get("ground_truth", "")) + + if count and len(queries) >= count: + break + + return queries, expected_tools, ground_truths + + +# ═══════════════════════════════════════════════════════════════════════════════ +# TESTS +# ═══════════════════════════════════════════════════════════════════════════════ + + +@pytest.fixture +def comparison_runner(): + """Create an agent comparison runner.""" + return AgentComparisonRunner() + + +class TestAgentComparison: + """Tests that compare single_agent vs reflection_agent.""" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_quick_comparison(self, comparison_runner): + """Quick comparison using 3 test cases.""" + quick_count = int(os.getenv("EVAL_QUICK_TEST_COUNT", "3")) + queries, expected_tools, _ = load_test_data(count=quick_count) + + metrics = await comparison_runner.run_comparison(queries, expected_tools) + + print("\n" + metrics.to_report()) + + # Both agents should have some level of success + assert metrics.agent_a_metrics.get("success_rate", 0) >= 0.5, \ + "Single agent success rate too low" + assert metrics.agent_b_metrics.get("success_rate", 0) >= 0.5, \ + "Reflection agent success rate too low" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_full_comparison(self, comparison_runner): + """Full comparison using all test cases.""" + queries, expected_tools, _ = load_test_data() + + metrics = await comparison_runner.run_comparison(queries, expected_tools) + + print("\n" + metrics.to_report()) + + # Store results for analysis + results_file = _eval_dir / "comparison_results.json" + with open(results_file, "w") as f: + json.dump({ + "agent_a": metrics.agent_a, + "agent_b": metrics.agent_b, + "test_count": metrics.test_count, + "agent_a_metrics": metrics.agent_a_metrics, + "agent_b_metrics": metrics.agent_b_metrics, + "differences": metrics.differences + }, f, indent=2) + + print(f"\nResults saved to: {results_file}") + + # Basic assertions + assert metrics.test_count > 0, "No tests were run" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_execution_time_comparison(self, comparison_runner): + """Compare execution times between agents.""" + queries, expected_tools, _ = load_test_data(count=3) + + metrics = await comparison_runner.run_comparison(queries, expected_tools) + + single_time = metrics.agent_a_metrics.get("avg_execution_time", 0) + reflection_time = metrics.agent_b_metrics.get("avg_execution_time", 0) + + print(f"\nExecution Time Comparison:") + print(f" Single Agent: {single_time:.2f}s") + print(f" Reflection Agent: {reflection_time:.2f}s") + print(f" Difference: {reflection_time - single_time:.2f}s") + + # Reflection agent is expected to take longer (more LLM calls) + # Just verify times are reasonable + assert single_time > 0, "Single agent should have non-zero execution time" + assert reflection_time > 0, "Reflection agent should have non-zero execution time" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_response_quality_comparison(self, comparison_runner): + """Compare response quality metrics between agents.""" + queries, expected_tools, _ = load_test_data(count=3) + + metrics = await comparison_runner.run_comparison(queries, expected_tools) + + print("\nResponse Quality Comparison:") + print(f" Single Agent Response Length: {metrics.agent_a_metrics.get('avg_response_length', 0):.0f}") + print(f" Reflection Agent Response Length: {metrics.agent_b_metrics.get('avg_response_length', 0):.0f}") + + # Both should produce responses + assert metrics.agent_a_metrics.get("avg_response_length", 0) > 0 + assert metrics.agent_b_metrics.get("avg_response_length", 0) > 0 + + +class TestSingleAgentOnly: + """Tests for single agent in isolation.""" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_single_agent_basic(self, comparison_runner): + """Test single agent with a basic query.""" + result = await comparison_runner._run_single_agent( + "Hello, I need help with my account" + ) + + assert result.success, f"Single agent failed: {result.error}" + assert len(result.response) > 0, "Response should not be empty" + print(f"\nSingle Agent Response: {result.response[:200]}...") + + +class TestReflectionAgentOnly: + """Tests for reflection agent in isolation.""" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_reflection_agent_basic(self, comparison_runner): + """Test reflection agent with a basic query.""" + result = await comparison_runner._run_reflection_agent( + "Hello, I need help with my account" + ) + + assert result.success, f"Reflection agent failed: {result.error}" + assert len(result.response) > 0, "Response should not be empty" + print(f"\nReflection Agent Response: {result.response[:200]}...") + + +# ═══════════════════════════════════════════════════════════════════════════════ +# STANDALONE EXECUTION +# ═══════════════════════════════════════════════════════════════════════════════ + +if __name__ == "__main__": + """Run comparison directly without pytest.""" + async def main(): + runner = AgentComparisonRunner() + queries, expected_tools, _ = load_test_data(count=3) + + print("Running Agent Comparison...") + print(f"Comparing: single_agent vs reflection_agent") + print(f"Test cases: {len(queries)}") + + metrics = await runner.run_comparison(queries, expected_tools) + print(metrics.to_report()) + + asyncio.run(main()) diff --git a/tests/evaluation/test_data.jsonl b/tests/evaluation/test_data.jsonl new file mode 100644 index 000000000..3bc2f1cfa --- /dev/null +++ b/tests/evaluation/test_data.jsonl @@ -0,0 +1,10 @@ +{"query": "I noticed my last invoice was higher than usual—can you help me understand why and what can be done about it?", "customer_id": "251", "expected_intent": "billing_inquiry", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_billing_summary", "search_knowledge_base"], "ground_truth": "Customer 251 (John Doe) has invoice showing $150 which is 2.5x usual. Agent should detect data overage (22GB vs 10GB cap), quote Data Overage Policy about retroactive upgrade within 15 days, and offer invoice adjustment or plan upgrade with pro-rata credit.", "category": "billing", "complexity": "medium"} +{"query": "My internet service seems slower than before—can you check what's happening?", "customer_id": "252", "expected_intent": "service_issue", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_data_usage", "search_knowledge_base"], "ground_truth": "Customer 252 (Jane Doe, Gold loyalty) has 1Gbps plan but service_status is 'slow'. Agent should check ServiceIncidents for open ticket, reference KB Troubleshooting Slow Internet, suggest speed test and reboot, escalate if below 25% of tier.", "category": "technical_support", "complexity": "medium"} +{"query": "I'm traveling abroad next month. What should I do about my phone plan?", "customer_id": "253", "expected_intent": "plan_inquiry", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products", "search_knowledge_base"], "ground_truth": "Customer 253 (Mark Doe, Bronze) has roaming_enabled=0. Agent should verify roaming not active, suggest International Roaming add-on, quote KB about activating 3+ days ahead, offer immediate activation with pro-rated charges.", "category": "products", "complexity": "medium"} +{"query": "I tried logging into my account, but it says I'm locked out. Can you help?", "customer_id": "254", "expected_intent": "security_issue", "expected_tools": ["get_customer_detail", "get_security_logs", "unlock_account", "search_knowledge_base"], "ground_truth": "Customer 254 (Alice Doe, Gold) has account_locked event in SecurityLogs from 12 min ago. Agent should follow Account Unlock Procedure: send 2FA code, verify identity, force password reset.", "category": "security", "complexity": "high"} +{"query": "Do I qualify for any discounts or promotions right now?", "customer_id": "255", "expected_intent": "promotion_inquiry", "expected_tools": ["get_customer_detail", "get_eligible_promotions", "get_promotions"], "ground_truth": "Customer 255 (Ron Doe, Gold loyalty) should qualify for promotions matching loyalty_level='Gold' with current dates. Agent should return Mobile Loyalty Discount (10%) and explain any future promos not yet active per Promotion Eligibility Guidelines.", "category": "promotions", "complexity": "low"} +{"query": "I want to return a product I recently purchased. What's the process?", "customer_id": "256", "expected_intent": "return_request", "expected_tools": ["get_customer_orders", "search_knowledge_base"], "ground_truth": "Customer 256 (Mary Doe, Silver) has Orders.order_status='returned' from 25 days ago within 30-day window. Agent should cite Return Policy and Process: 7-10 business days for refund, escalate if over 10 days passed.", "category": "orders", "complexity": "low"} +{"query": "Customer 251, what's my billing summary?", "customer_id": "251", "expected_intent": "billing_inquiry", "expected_tools": ["get_billing_summary", "get_customer_detail"], "ground_truth": "Customer 251 (John Doe) has $100 balance remaining ($50 already paid of $150 invoice). Agent should retrieve and present current outstanding balance across all subscriptions.", "category": "billing", "complexity": "low"} +{"query": "I keep getting dropped calls whenever I'm downtown. Can you help fix this?", "customer_id": "257", "expected_intent": "support_request", "expected_tools": ["get_support_tickets", "get_customer_detail", "create_support_ticket", "search_knowledge_base"], "ground_truth": "Customer 257 (Tom Smith, Silver) has SupportTickets category 'call_drop'. Agent should follow KB Dropped Call Investigation Workflow, capture times/locations, escalate to RF engineering, apply credit per Service Reliability SLA Credit Matrix if systemic.", "category": "support", "complexity": "medium"} +{"query": "My service is suspended because I missed a payment. Can you help restore it and waive the late fee?", "customer_id": "258", "expected_intent": "payment_issue", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_invoice_payments", "search_knowledge_base"], "ground_truth": "Customer 258 (Sara Lee, Gold) has invoice unpaid >14 days, 2 failed payment attempts, account status='inactive'. Agent should reference Payment Failure & Reinstatement Rules and Late Payment Fee Policy, offer first-time waiver eligibility and hardship plan per Financial Hardship Payment Plan Procedure.", "category": "billing", "complexity": "high"} +{"query": "I received a $150 bill due to data overage. Can you explain and help reduce it?", "customer_id": "259", "expected_intent": "data_overage", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_data_usage", "search_knowledge_base"], "ground_truth": "Customer 259 (Alex Brown, Bronze) used ~22GB vs 10GB cap = 12GB over. Agent should quote Data Overage Policy: can switch to higher tier retroactively within 15 days of invoice, overage will be re-rated. Upsell larger plan or unlimited bundle.", "category": "billing", "complexity": "high"} diff --git a/tests/evaluation/test_scenario_evaluation.py b/tests/evaluation/test_scenario_evaluation.py new file mode 100644 index 000000000..08cd35262 --- /dev/null +++ b/tests/evaluation/test_scenario_evaluation.py @@ -0,0 +1,2020 @@ +""" +Scenario-Based Agent Evaluation + +This module provides comprehensive evaluation for agents: +1. Goal-Based (Outcome): Did the user get what they needed? (LLM-as-Judge or keyword matching) +2. Process-Based (Tool Accuracy): Did the agent use the right tools? (LLM-as-Judge or F1 score) + +Uses the AgentTestRunner for a consistent interface across all agents. +Optionally uses Azure AI Foundry LLM-as-Judge evaluators for more sophisticated evaluation. + +Usage: + cd tests/evaluation + uv run pytest test_scenario_evaluation.py -v -s + + # With LLM-as-Judge (set EVAL_USE_LLM_JUDGE=true in .env) + uv run pytest test_scenario_evaluation.py::TestAgentComparison -v -s +""" + +import asyncio +import json +import os +import sys +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Optional + +import pytest +from dotenv import load_dotenv + +# ═══════════════════════════════════════════════════════════════════════════════ +# PATH SETUP +# ═══════════════════════════════════════════════════════════════════════════════ + +_eval_dir = Path(__file__).parent.resolve() +_tests_dir = _eval_dir.parent +_workspace_root = _tests_dir.parent +_agentic_ai_dir = _workspace_root / "agentic_ai" + +sys.path.insert(0, str(_agentic_ai_dir)) +sys.path.insert(0, str(_tests_dir)) +sys.path.insert(0, str(_eval_dir)) + +load_dotenv(_eval_dir / ".env") + +# Import the generic agent runner (ToolCallTracker is bundled in the runner) +from agent_runner import AgentTestRunner, QueryResult + +# Import LLM judge evaluator +try: + from llm_judge_evaluator import ( + LLMJudgeEvaluator, + ToolCall, + ToolDefinition, + EvaluationResult, + EVALUATORS_AVAILABLE, + ) + LLM_JUDGE_AVAILABLE = EVALUATORS_AVAILABLE +except ImportError: + LLM_JUDGE_AVAILABLE = False + +# Check if LLM judge should be used +USE_LLM_JUDGE = os.getenv("EVAL_USE_LLM_JUDGE", "false").lower() in ("true", "1", "yes") + + +# ═══════════════════════════════════════════════════════════════════════════════ +# MCP TOOL DEFINITIONS (for LLM judge) +# ═══════════════════════════════════════════════════════════════════════════════ + +MCP_TOOL_DEFINITIONS = [ + {"name": "get_customer_detail", "description": "Get customer profile and account information"}, + {"name": "get_billing_summary", "description": "Get billing and invoice summary for a customer"}, + {"name": "get_subscription_detail", "description": "Get subscription plan details including data caps and features"}, + {"name": "get_data_usage", "description": "Get current data usage statistics"}, + {"name": "get_security_logs", "description": "Get security audit logs for account access attempts"}, + {"name": "unlock_account", "description": "Unlock a customer account after verification"}, + {"name": "get_products", "description": "List available products and add-ons"}, + {"name": "get_support_tickets", "description": "Get support ticket history"}, + {"name": "search_knowledge", "description": "Search the knowledge base for policies and procedures"}, +] + + +# ═══════════════════════════════════════════════════════════════════════════════ +# SCENARIO DEFINITIONS +# ═══════════════════════════════════════════════════════════════════════════════ + + +@dataclass +class ConversationTurn: + """A single turn in the conversation.""" + user_message: str + expected_tool_calls: list[str] = field(default_factory=list) + expected_keywords: list[str] = field(default_factory=list) # Keywords in response + + +@dataclass +class Scenario: + """A complete customer scenario for evaluation.""" + id: str + name: str + description: str + customer_id: int + + # Conversation flow + turns: list[ConversationTurn] = field(default_factory=list) + + # Expected tools across entire conversation + expected_tools: list[str] = field(default_factory=list) + + # Expected outcome keywords (should appear in final response) + success_keywords: list[str] = field(default_factory=list) + + # Expected resolution (for AI evaluation) + expected_resolution: str = "" + + # Ground truth solution - the ideal/correct solution + ground_truth_solution: str = "" + + # Scoring rubric - criteria for evaluating solution accuracy + scoring_rubric: str = "" + + +# Define scenarios based on customer_scenarios.md and data_seeding.py +# MCP tool names: get_customer_detail, get_subscription_detail, get_data_usage, +# get_billing_summary, get_security_logs, unlock_account, get_products, +# search_knowledge, get_support_tickets, etc. +# +# Customer ID ranges: +# - 251-254: Documented scenarios from customer_scenarios.md +# - 1-50: Randomly generated customers in data_seeding.py (use for new scenarios) +SCENARIOS = [ + # ═══════════════════════════════════════════════════════════════════════════════ + # BILLING & PAYMENT SCENARIOS (5 scenarios) + # ═══════════════════════════════════════════════════════════════════════════════ + Scenario( + id="billing_high_invoice", + name="Invoice Higher Than Usual", + description="Customer 251 has invoice $150, 2.5x the usual amount", + customer_id=251, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 251. I noticed my last invoice was $150, which is much higher than usual. Can you help me understand why?", + expected_tool_calls=["get_billing_summary", "get_data_usage"], + expected_keywords=["invoice", "overage", "data", "usage"], + ), + ], + expected_tools=[ + "get_customer_detail", + "get_billing_summary", + "get_subscription_detail", + "get_data_usage", + "search_knowledge", + ], + success_keywords=["overage", "data", "upgrade", "adjustment", "22", "10"], + expected_resolution="Identify data overage (22GB vs 10GB cap), quote Data Overage Policy, offer adjustment or plan upgrade", + ground_truth_solution="""The customer's invoice is $150 instead of the usual $60 because of data overage charges. +Key facts to communicate: +1. The customer's plan has a 10GB data cap +2. The customer used 22GB this billing cycle (12GB over the limit) +3. Overage charges of $7.50/GB apply per the Data Overage Policy +4. The additional $90 in charges (12GB x $7.50) explains the higher bill + +Recommended solutions: +- Offer a one-time courtesy adjustment (if first offense) +- Recommend upgrading to a higher data plan or unlimited plan +- Set up data usage alerts to prevent future overages""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Correctly identifies overage (22GB vs 10GB), explains charges clearly, offers both adjustment AND upgrade options +4 - Good: Identifies overage and explains charges, offers at least one solution option +3 - Adequate: Identifies overage as the cause but missing specific numbers or only partial solution +2 - Poor: Vague explanation, doesn't clearly identify the cause or missing key details +1 - Fail: Incorrect explanation or completely unhelpful response""", + ), + + Scenario( + id="billing_payment_history", + name="Payment History Inquiry", + description="Customer wants to see recent payment history and payment methods", + customer_id=5, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 5. Can you show me my recent payments? I want to make sure they all went through.", + expected_tool_calls=["get_billing_summary"], + expected_keywords=["payment", "successful", "history"], + ), + ], + expected_tools=["get_customer_detail", "get_billing_summary"], + success_keywords=["payment", "successful", "credit_card", "amount", "date"], + expected_resolution="Retrieve payment history and confirm successful payments", + ground_truth_solution="""Show the customer their recent payment history. +Key information to provide: +1. List recent payments with dates and amounts +2. Confirm payment methods used (credit card, bank transfer, etc.) +3. Identify any failed or pending payments +4. Provide current account balance if any + +Helpful actions: +- Confirm all payments were successful +- Mention autopay option if not enabled +- Offer to send payment receipt copies if needed""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows payment history with dates, amounts, methods, and confirms all went through +4 - Good: Shows payment history and confirms status +3 - Adequate: Provides payment information but incomplete details +2 - Poor: Vague response without specific payment details +1 - Fail: Doesn't provide payment information""", + ), + + Scenario( + id="billing_autopay_setup", + name="Autopay Setup Request", + description="Customer wants to enable automatic payments", + customer_id=10, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 10. I keep forgetting to pay my bill on time. Can you help me set up autopay?", + expected_tool_calls=["get_billing_summary", "search_knowledge"], + expected_keywords=["autopay", "automatic", "payment"], + ), + ], + expected_tools=["get_customer_detail", "get_billing_summary", "get_subscription_detail", "search_knowledge"], + success_keywords=["autopay", "automatic", "$5", "discount", "enable"], + expected_resolution="Check current autopay status, explain autopay benefits including $5 discount, guide through setup", + ground_truth_solution="""Help customer set up automatic payments. +Key information to provide: +1. Current autopay status (enabled or disabled) +2. Autopay includes a $5 monthly discount +3. Explain how autopay works (auto-charge on due date) + +Required actions: +- Check current billing/subscription status +- Explain the $5 autopay discount +- Guide through the setup process +- Confirm payment method on file""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Checks status, mentions $5 discount, explains benefits, and guides through setup +4 - Good: Explains autopay benefits and how to set it up +3 - Adequate: Provides basic autopay information +2 - Poor: Generic response without checking account +1 - Fail: Doesn't help with autopay setup""", + ), + + Scenario( + id="billing_overdue_invoice", + name="Overdue Invoice Question", + description="Customer has overdue invoices and wants to understand implications", + customer_id=15, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 15. I received a notice about an overdue invoice. What happens if I don't pay soon?", + expected_tool_calls=["get_billing_summary"], + expected_keywords=["overdue", "payment", "service"], + ), + ], + expected_tools=["get_customer_detail", "get_billing_summary", "search_knowledge"], + success_keywords=["overdue", "payment", "suspension", "late", "fee"], + expected_resolution="Show overdue invoices, explain late payment consequences, offer payment options", + ground_truth_solution="""Address overdue invoice concerns. +Key information to provide: +1. List overdue invoices with amounts and due dates +2. Explain late fee policy (if applicable) +3. Potential service suspension after 30+ days overdue +4. Payment options available + +Recommended actions: +- Show specific overdue amount +- Explain consequences (late fees, service suspension) +- Offer payment plan if large amount +- Process payment immediately if customer wants""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows overdue details, explains consequences, and offers solutions/payment options +4 - Good: Explains consequences and helps with payment +3 - Adequate: Addresses concern but missing specifics +2 - Poor: Generic response without checking account +1 - Fail: Doesn't address the overdue concern""", + ), + + Scenario( + id="billing_refund_request", + name="Refund Request for Service Issue", + description="Customer wants refund for days without service", + customer_id=20, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 20. I was without internet for 3 days last week. Can I get a refund or credit for those days?", + expected_tool_calls=["get_support_tickets", "get_billing_summary"], + expected_keywords=["credit", "refund", "outage", "service"], + ), + ], + expected_tools=["get_customer_detail", "get_support_tickets", "get_subscription_detail", "get_billing_summary"], + success_keywords=["credit", "refund", "outage", "days", "pro-rated"], + expected_resolution="Verify outage via tickets/incidents, calculate pro-rated credit, apply to account", + ground_truth_solution="""Process refund request for service outage. +Key information to verify: +1. Check support tickets for reported outage +2. Verify service incident records +3. Calculate pro-rated credit (3 days of monthly fee) + +Recommended actions: +- Verify the outage occurred (via tickets or incidents) +- Calculate appropriate credit amount +- Apply credit to next invoice +- Apologize for the inconvenience +- Confirm credit will appear on next bill""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Verifies outage, calculates pro-rated credit, applies credit, and confirms +4 - Good: Acknowledges issue and offers appropriate credit +3 - Adequate: Offers to help with credit but missing verification +2 - Poor: Generic response without checking history +1 - Fail: Doesn't address the refund request""", + ), + + # ═══════════════════════════════════════════════════════════════════════════════ + # INTERNET & CONNECTIVITY SCENARIOS (5 scenarios) + # ═══════════════════════════════════════════════════════════════════════════════ + Scenario( + id="internet_slow", + name="Internet Slower Than Before", + description="Customer 252 experiencing slow internet on 1Gbps tier", + customer_id=252, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 252. My internet has been really slow lately. I'm paying for 1Gbps but it feels much slower.", + expected_tool_calls=["get_subscription_detail", "get_support_tickets"], + expected_keywords=["speed", "issue", "incident", "troubleshoot"], + ), + ], + expected_tools=[ + "get_customer_detail", + "get_subscription_detail", + "get_support_tickets", + "search_knowledge", + ], + success_keywords=["speed", "troubleshoot", "reboot", "test", "incident"], + expected_resolution="Check subscription status, find open incident, provide troubleshooting steps", + ground_truth_solution="""The customer is on a 1Gbps plan but experiencing slow speeds. +Key facts to communicate: +1. There is an existing open service incident affecting the customer's area +2. The incident was reported on April 17 and is still under investigation +3. The service status shows 'slow' indicating a known issue + +Recommended actions: +- Acknowledge the known service issue and apologize for inconvenience +- Provide basic troubleshooting steps (restart router, check cables, test wired connection) +- Offer to create/escalate a support ticket for priority resolution +- Mention potential service credit once issue is resolved""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Identifies existing incident, provides troubleshooting steps, offers to escalate AND mentions potential credit +4 - Good: Identifies incident and provides troubleshooting, offers at least one proactive step +3 - Adequate: Acknowledges issue and provides some troubleshooting steps +2 - Poor: Generic troubleshooting without checking account status or incidents +1 - Fail: Unhelpful response or doesn't address the speed issue""", + ), + + Scenario( + id="internet_upgrade_inquiry", + name="Internet Speed Upgrade Options", + description="Customer wants to upgrade internet speed", + customer_id=25, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 25. I work from home and my current internet is too slow for video calls. What upgrade options do I have?", + expected_tool_calls=["get_subscription_detail", "get_products"], + expected_keywords=["upgrade", "speed", "plan", "price"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_products", "search_knowledge"], + success_keywords=["upgrade", "Mbps", "Gbps", "Pro", "Ultimate", "price"], + expected_resolution="Check current plan, show available upgrade options with pricing", + ground_truth_solution="""Help customer upgrade their internet plan. +Key information to provide: +1. Current plan details (speed tier, price) +2. Available upgrade options: + - Fiber Internet - Basic: 100 Mbps @ $49.99/month + - Fiber Internet - Pro: 500 Mbps @ $79.99/month + - Fiber Internet - Ultimate: 1 Gbps @ $119.99/month +3. For video calls, recommend at least Pro (500 Mbps) + +Recommended actions: +- Show price difference from current plan +- Explain upgrade benefits (WiFi 6 router, priority support) +- Offer any applicable promotions (loyalty upgrade, new customer discount) +- Upgrades take effect within 24 hours""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows current plan, presents upgrade options with pricing, recommends based on need, mentions promotions +4 - Good: Shows options with pricing and makes recommendation +3 - Adequate: Lists upgrade options but missing personalization +2 - Poor: Generic product info without checking current plan +1 - Fail: Doesn't provide helpful upgrade information""", + ), + + Scenario( + id="internet_router_reset", + name="Router Reset Help", + description="Customer needs help resetting their router", + customer_id=30, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 30. My router isn't working and I think I need to reset it. How do I do that?", + expected_tool_calls=["search_knowledge"], + expected_keywords=["reset", "button", "router"], + ), + ], + expected_tools=["get_customer_detail", "search_knowledge"], + success_keywords=["reset", "button", "10 seconds", "paperclip", "factory", "settings"], + expected_resolution="Provide step-by-step router reset instructions from knowledge base", + ground_truth_solution="""Help customer reset their router. +Steps to communicate: +1. Locate the reset button on the back of the router +2. Use a paperclip to press and hold the button for 10 seconds +3. Wait for the router to restart (lights will blink) +4. Router returns to factory settings +5. Reconnect using default WiFi name and password on router label + +Additional help: +- If issues persist after reset, contact support at 1-800-CONTOSO +- Offer to schedule a technician if customer is uncomfortable doing it themselves""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Provides complete step-by-step instructions, mentions factory settings warning, offers additional help +4 - Good: Provides reset steps and basic guidance +3 - Adequate: Gives reset instructions but incomplete +2 - Poor: Vague instructions without specific steps +1 - Fail: Doesn't help with router reset""", + ), + + Scenario( + id="internet_outage_report", + name="Internet Outage Report", + description="Customer reporting complete internet outage", + customer_id=35, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 35. My internet is completely down! Nothing is working. Is there an outage in my area?", + expected_tool_calls=["get_subscription_detail", "get_support_tickets"], + expected_keywords=["outage", "incident", "status"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge"], + success_keywords=["outage", "incident", "ticket", "technician", "status"], + expected_resolution="Check for area outages, create support ticket if needed, provide ETA", + ground_truth_solution="""Handle internet outage report. +Key actions: +1. Check subscription service status +2. Look for existing service incidents +3. Check if other support tickets exist for this customer + +If outage confirmed: +- Apologize for the inconvenience +- Provide estimated restoration time +- Offer to notify when service is restored + +If no known outage: +- Create a new support ticket +- Provide troubleshooting steps +- Offer technician visit if needed +- Mention service credit for extended outages""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Checks outage status, creates ticket if needed, provides ETA, offers follow-up +4 - Good: Checks status and takes appropriate action +3 - Adequate: Acknowledges issue and offers some help +2 - Poor: Generic response without checking system +1 - Fail: Doesn't address the outage report""", + ), + + Scenario( + id="internet_static_ip", + name="Static IP Request", + description="Customer needs a static IP address for work", + customer_id=40, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 40. I need a static IP address for my home server. Do you offer that?", + expected_tool_calls=["get_subscription_detail", "get_products"], + expected_keywords=["static", "IP", "feature"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], + success_keywords=["static", "IP", "Pro", "Ultimate", "upgrade", "feature"], + expected_resolution="Check current plan, explain static IP is included in Pro/Ultimate plans", + ground_truth_solution="""Help customer get a static IP address. +Key information: +1. Static IP is included in: + - Fiber Internet - Pro ($79.99/month) - includes 1 static IP + - Fiber Internet - Ultimate ($119.99/month) - includes 1 static IP + - Business Internet - Enterprise ($299.99/month) - includes static IP block +2. Basic plan does not include static IP + +Recommended actions: +- Check current plan +- If on Basic, recommend upgrade to Pro +- Explain static IP benefits and configuration +- Offer to process upgrade immediately""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Checks plan, explains which plans include static IP, recommends appropriate option +4 - Good: Explains static IP availability and recommends upgrade +3 - Adequate: Mentions static IP but missing plan details +2 - Poor: Generic response without specific information +1 - Fail: Doesn't address the static IP request""", + ), + + # ═══════════════════════════════════════════════════════════════════════════════ + # MOBILE & ROAMING SCENARIOS (4 scenarios) + # ═══════════════════════════════════════════════════════════════════════════════ + Scenario( + id="roaming_travel", + name="Travelling Abroad - Needs Roaming", + description="Customer 253 traveling to Spain, needs roaming setup", + customer_id=253, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 253. I'm traveling to Spain in 2 days and need to know about international roaming.", + expected_tool_calls=["get_subscription_detail", "get_products"], + expected_keywords=["roaming", "international", "activate"], + ), + ], + expected_tools=[ + "get_customer_detail", + "get_subscription_detail", + "get_products", + "search_knowledge", + ], + success_keywords=["roaming", "international", "activate", "add-on"], + expected_resolution="Check roaming not enabled, suggest International Roaming add-on, explain 3-day activation requirement", + ground_truth_solution="""The customer needs international roaming enabled before traveling to Spain in 2 days. +Key facts to communicate: +1. International roaming is currently NOT enabled on their account +2. Roaming packages typically require 3 days to activate (customer is cutting it close) +3. Spain is covered under European roaming options +4. Available add-ons include voice, text, and data packages + +Recommended actions: +- Urgently enable international roaming on the account +- Recommend appropriate roaming package for Spain (Europe zone) +- Warn about the activation timeline (may need to request expedited activation) +- Explain roaming rates and usage alerts to avoid bill shock""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Identifies roaming is off, explains urgency (3-day activation), offers to enable AND recommends specific package +4 - Good: Identifies roaming status and urgency, offers to enable roaming +3 - Adequate: Identifies roaming is not enabled and offers to help activate +2 - Poor: Generic roaming information without checking account status +1 - Fail: Doesn't address the roaming request or provides incorrect information""", + ), + + Scenario( + id="mobile_data_usage", + name="Mobile Data Usage Check", + description="Customer wants to check mobile data usage before cycle ends", + customer_id=45, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 45. How much data have I used this month? I don't want to go over my limit.", + expected_tool_calls=["get_data_usage", "get_subscription_detail"], + expected_keywords=["data", "usage", "GB", "limit"], + ), + ], + expected_tools=["get_customer_detail", "get_data_usage", "get_subscription_detail"], + success_keywords=["data", "used", "remaining", "GB", "cap"], + expected_resolution="Show current data usage vs plan limit, warn if close to limit", + ground_truth_solution="""Check customer's data usage. +Key information to provide: +1. Current data usage for this billing cycle +2. Data cap from subscription plan +3. Days remaining in billing cycle +4. Percentage of data used + +If close to limit: +- Warn about overage charges +- Suggest data-saving tips +- Offer unlimited data upgrade option +- Explain how to set up usage alerts""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows usage, cap, remaining, and provides proactive advice based on status +4 - Good: Shows usage and limit with clear comparison +3 - Adequate: Provides data usage information +2 - Poor: Vague or incomplete information +1 - Fail: Doesn't provide data usage""", + ), + + Scenario( + id="mobile_upgrade_premium", + name="Mobile Plan Upgrade", + description="Customer wants to upgrade mobile plan for more data", + customer_id=3, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 3. I keep running out of data. What mobile plans with more data do you have?", + expected_tool_calls=["get_subscription_detail", "get_products"], + expected_keywords=["plan", "unlimited", "data", "price"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], + success_keywords=["Premium", "unlimited", "data", "59.99", "upgrade"], + expected_resolution="Show current plan, recommend Mobile Plan - Premium with unlimited data", + ground_truth_solution="""Help customer upgrade mobile plan. +Key information: +1. Current plan: Mobile Plan - Essential (5GB data @ $29.99/month) +2. Recommended upgrade: Mobile Plan - Premium ($59.99/month) + - Unlimited data + - International roaming included + - 5G Priority + - 50GB hotspot + +Recommended actions: +- Explain price difference ($30/month more) +- Highlight unlimited data benefit +- Mention included international roaming +- Offer to process upgrade immediately""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows current plan, recommends Premium with pricing, highlights benefits +4 - Good: Provides upgrade options with clear comparison +3 - Adequate: Mentions upgrade options but missing details +2 - Poor: Generic product info without personalization +1 - Fail: Doesn't help with upgrade""", + ), + + Scenario( + id="mobile_hotspot_question", + name="Mobile Hotspot Inquiry", + description="Customer asking about hotspot feature on their plan", + customer_id=8, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 8. Does my mobile plan include hotspot? I need to use it for my laptop.", + expected_tool_calls=["get_subscription_detail"], + expected_keywords=["hotspot", "plan", "feature"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], + success_keywords=["hotspot", "included", "GB", "tethering"], + expected_resolution="Check plan details, explain hotspot inclusion based on plan tier", + ground_truth_solution="""Answer hotspot question. +Key information based on mobile plan: +- Essential plan: Hotspot NOT included (or limited) +- Premium plan: 50GB hotspot included + +Actions: +- Check customer's current mobile plan +- Explain hotspot feature availability +- If not included, offer Premium upgrade +- Provide instructions on enabling hotspot if available""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Checks plan, explains hotspot status, provides usage info or upgrade option +4 - Good: Explains hotspot availability for their plan +3 - Adequate: Addresses hotspot question generally +2 - Poor: Vague response without checking plan +1 - Fail: Doesn't address hotspot question""", + ), + + # ═══════════════════════════════════════════════════════════════════════════════ + # ACCOUNT & SECURITY SCENARIOS (4 scenarios) + # ═══════════════════════════════════════════════════════════════════════════════ + Scenario( + id="account_locked", + name="Account Locked After Failed Logins", + description="Customer 254 locked out after multiple failed login attempts", + customer_id=254, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 254. I can't log into my account - it says it's locked!", + expected_tool_calls=["get_security_logs", "unlock_account"], + expected_keywords=["locked", "security", "unlock", "password"], + ), + ], + expected_tools=[ + "get_customer_detail", + "get_security_logs", + "unlock_account", + "search_knowledge", + ], + success_keywords=["unlock", "password", "security", "2FA", "reset"], + expected_resolution="Query security logs, verify identity, unlock account, recommend password reset and 2FA", + ground_truth_solution="""The customer's account is locked due to multiple failed login attempts. +Key facts to communicate: +1. Security logs show multiple failed login attempts triggering automatic lockout +2. This is a security feature to protect the account +3. The account can be unlocked after identity verification + +Required actions: +- Verify customer identity (already done via customer ID) +- Unlock the account using unlock_account tool +- Confirm the account is now accessible + +Recommended follow-up: +- Suggest password reset if customer forgot password +- Recommend enabling 2FA for additional security +- Advise using password manager to prevent future lockouts""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Verifies identity, unlocks account, confirms success, AND provides security recommendations (password reset, 2FA) +4 - Good: Verifies identity, unlocks account, and provides at least one security recommendation +3 - Adequate: Unlocks the account and confirms it's accessible +2 - Poor: Attempts to help but doesn't actually unlock the account +1 - Fail: Doesn't address the lockout or provides incorrect instructions""", + ), + + Scenario( + id="account_security_check", + name="Security Audit Request", + description="Customer concerned about account security after hearing about data breaches", + customer_id=12, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 12. I heard about data breaches in the news. Can you check if my account is secure?", + expected_tool_calls=["get_security_logs"], + expected_keywords=["security", "login", "access"], + ), + ], + expected_tools=["get_customer_detail", "get_security_logs", "search_knowledge"], + success_keywords=["security", "login", "attempts", "2FA", "password"], + expected_resolution="Review security logs, confirm no suspicious activity, recommend security best practices", + ground_truth_solution="""Perform security audit for customer. +Key actions: +1. Review security logs for suspicious activity +2. Check for failed login attempts from unknown locations +3. Verify no unauthorized access + +Provide security recommendations: +- Enable 2FA if not already enabled +- Use strong, unique password +- Update password every 90 days +- Never share credentials +- Monitor account for suspicious activity + +Reassure customer and explain security measures in place.""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Reviews logs, reports findings, provides comprehensive security recommendations +4 - Good: Checks security status and provides recommendations +3 - Adequate: Reviews security but limited recommendations +2 - Poor: Generic security advice without checking account +1 - Fail: Doesn't address security concern""", + ), + + Scenario( + id="account_update_contact", + name="Update Contact Information", + description="Customer wants to update email and phone number", + customer_id=18, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 18. I have a new email and phone number. Can you update my account information?", + expected_tool_calls=["get_customer_detail"], + expected_keywords=["update", "contact", "email", "phone"], + ), + ], + expected_tools=["get_customer_detail"], + success_keywords=["update", "email", "phone", "verify", "confirm"], + expected_resolution="Show current info, explain update process, request new details", + ground_truth_solution="""Help customer update contact information. +Key actions: +1. Retrieve current contact details +2. Verify customer identity +3. Collect new email and phone number +4. Explain verification process for new contact info + +Security note: +- New email may require verification +- Update affects notifications and billing alerts +- Password reset links go to email on file +- Confirm all communication preferences""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows current info, requests new details, explains verification, updates preferences +4 - Good: Helps with update and explains process +3 - Adequate: Acknowledges request and provides guidance +2 - Poor: Generic response without checking current info +1 - Fail: Doesn't help with update""", + ), + + Scenario( + id="account_paperless_billing", + name="Paperless Billing Setup", + description="Customer wants to switch to paperless billing", + customer_id=22, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 22. I want to go paperless and stop receiving paper bills. How do I do that?", + expected_tool_calls=["get_customer_detail"], + expected_keywords=["paperless", "email", "billing"], + ), + ], + expected_tools=["get_customer_detail", "search_knowledge"], + success_keywords=["paperless", "email", "billing", "enabled", "notification"], + expected_resolution="Check current settings, enable paperless billing, confirm email on file", + ground_truth_solution="""Enable paperless billing for customer. +Key actions: +1. Check current billing preferences +2. Verify email address on file +3. Enable paperless billing +4. Explain paperless billing benefits + +Confirm: +- Bills will be sent to email on file +- Paper bills will stop within 1-2 billing cycles +- Can view all bills online anytime +- Email notifications for new bills""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Checks settings, confirms email, enables paperless, explains benefits +4 - Good: Enables paperless and confirms changes +3 - Adequate: Provides guidance on paperless billing +2 - Poor: Generic info without checking account +1 - Fail: Doesn't help with paperless setup""", + ), + + # ═══════════════════════════════════════════════════════════════════════════════ + # TV & STREAMING SCENARIOS (2 scenarios) + # ═══════════════════════════════════════════════════════════════════════════════ + Scenario( + id="tv_channel_lineup", + name="TV Channel Lineup Question", + description="Customer asking about available channels on their TV plan", + customer_id=28, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 28. What channels do I get with my TV streaming plan?", + expected_tool_calls=["get_subscription_detail", "get_products"], + expected_keywords=["channels", "TV", "streaming"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], + success_keywords=["channels", "streaming", "screens", "replay"], + expected_resolution="Check TV subscription, list included channels and features", + ground_truth_solution="""Show TV streaming plan details. +TV Streaming plans: +- Basic ($34.99/month): 50+ channels, 2 screens, 7-day replay +- Premium ($64.99/month): 150+ channels, 4 screens, 30-day replay, sports, movies + +Actions: +- Check current TV subscription +- List included channels/features +- Mention upgrade options if on Basic +- Explain how to access streaming app""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows plan details, lists features, mentions upgrade if applicable +4 - Good: Explains included channels and features +3 - Adequate: Provides plan information +2 - Poor: Generic TV info without checking plan +1 - Fail: Doesn't address channel question""", + ), + + Scenario( + id="tv_add_sports", + name="Add Sports Package", + description="Customer wants to add sports channels to TV plan", + customer_id=32, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 32. I want to watch football games. Do you have a sports package I can add?", + expected_tool_calls=["get_subscription_detail", "get_products"], + expected_keywords=["sports", "package", "channels"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], + success_keywords=["sports", "Premium", "channels", "upgrade"], + expected_resolution="Check current TV plan, explain sports is in Premium, offer upgrade", + ground_truth_solution="""Help customer add sports channels. +Key information: +- Sports package is included in TV Streaming - Premium ($64.99/month) +- Basic plan does not include sports channels + +Actions: +- Check current TV subscription +- If on Basic, offer upgrade to Premium +- Premium includes sports package plus movie channels +- Also includes 4 screens and 30-day replay +- Calculate price difference from current plan""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Checks plan, explains sports in Premium, shows pricing, offers to upgrade +4 - Good: Explains sports availability and upgrade option +3 - Adequate: Mentions sports package info +2 - Poor: Generic info without checking current plan +1 - Fail: Doesn't help with sports request""", + ), + + # ═══════════════════════════════════════════════════════════════════════════════ + # BUNDLE & PROMOTION SCENARIOS (3 scenarios) + # ═══════════════════════════════════════════════════════════════════════════════ + Scenario( + id="bundle_inquiry", + name="Bundle Package Inquiry", + description="Customer interested in bundling services", + customer_id=38, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 38. I have internet and mobile separately. Would I save money if I bundle them?", + expected_tool_calls=["get_subscription_detail", "get_products"], + expected_keywords=["bundle", "save", "discount"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], + success_keywords=["bundle", "Family Complete", "discount", "save", "$199.99"], + expected_resolution="Show current services, calculate potential savings with bundle", + ground_truth_solution="""Help customer understand bundle savings. +Bundle option: +- Bundle - Family Complete: $199.99/month + - 500Mbps Internet + - 150+ TV Channels + - 2 Unlimited Mobile Lines + - 20% discount vs individual services + +Actions: +- Check current subscriptions and total cost +- Calculate potential savings with bundle +- Explain bundle includes more than current services +- Show value proposition +- Offer to switch to bundle if interested""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows current cost, calculates savings, explains bundle benefits, offers to switch +4 - Good: Explains bundle options and potential savings +3 - Adequate: Provides bundle information +2 - Poor: Generic bundle info without checking current services +1 - Fail: Doesn't help with bundle inquiry""", + ), + + Scenario( + id="promotion_eligibility", + name="Promotion Eligibility Check", + description="Customer asking about current promotions they qualify for", + customer_id=42, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 42. Are there any promotions or discounts I'm eligible for?", + expected_tool_calls=["get_customer_detail", "get_subscription_detail"], + expected_keywords=["promotion", "discount", "offer"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], + success_keywords=["promotion", "discount", "loyalty", "offer", "eligible"], + expected_resolution="Check loyalty level, current services, find applicable promotions", + ground_truth_solution="""Check customer eligibility for promotions. +Available promotions: +1. New Customer - 20% Off (if new customer) +2. Bundle & Save - $50/month off (if 3+ services) +3. Loyalty Reward - Free speed upgrade (if Gold/Platinum) +4. Refer a Friend - $100 credit + +Actions: +- Check loyalty level (Bronze/Silver/Gold/Platinum) +- Check number of active services +- Identify applicable promotions +- Explain how to take advantage of offers""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Checks eligibility, lists applicable promos, explains how to apply +4 - Good: Identifies promotions customer qualifies for +3 - Adequate: Mentions available promotions +2 - Poor: Generic promo list without checking eligibility +1 - Fail: Doesn't help with promotion inquiry""", + ), + + Scenario( + id="loyalty_benefits", + name="Loyalty Program Benefits", + description="Customer asking about loyalty program benefits", + customer_id=48, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 48. I've been with you for years. What loyalty benefits do I get?", + expected_tool_calls=["get_customer_detail"], + expected_keywords=["loyalty", "benefits", "level"], + ), + ], + expected_tools=["get_customer_detail", "get_products", "search_knowledge"], + success_keywords=["loyalty", "Gold", "Silver", "Platinum", "benefits", "upgrade"], + expected_resolution="Check loyalty level, explain tier benefits, mention upgrade path", + ground_truth_solution="""Show loyalty program benefits. +Loyalty tiers: +- Bronze: Basic support +- Silver: Priority support, occasional discounts +- Gold: 24/7 VIP support, free speed upgrades, special promotions +- Platinum: All Gold benefits plus dedicated account manager + +Actions: +- Check customer's current loyalty level +- Explain benefits of their tier +- Mention how to reach next tier +- Highlight current Gold/Platinum promotion (free speed upgrade)""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Shows loyalty level, explains tier benefits, mentions upgrade path and current promos +4 - Good: Explains loyalty benefits for their tier +3 - Adequate: Provides loyalty program info +2 - Poor: Generic loyalty info without checking level +1 - Fail: Doesn't address loyalty question""", + ), + + # ═══════════════════════════════════════════════════════════════════════════════ + # SUPPORT TICKET SCENARIOS (2 scenarios) + # ═══════════════════════════════════════════════════════════════════════════════ + Scenario( + id="support_ticket_status", + name="Support Ticket Status Check", + description="Customer checking status of existing support ticket", + customer_id=6, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 6. I opened a support ticket a few days ago. Can you check the status?", + expected_tool_calls=["get_support_tickets"], + expected_keywords=["ticket", "status", "open"], + ), + ], + expected_tools=["get_customer_detail", "get_support_tickets"], + success_keywords=["ticket", "status", "open", "pending", "resolved"], + expected_resolution="Find open tickets, provide status update, explain next steps", + ground_truth_solution="""Check support ticket status. +Key actions: +1. Look up open/pending support tickets +2. Provide ticket number and status +3. Explain current stage of resolution +4. Provide expected resolution timeline + +If ticket is pending: +- Explain what's being done +- Offer to escalate if delayed +- Provide contact for urgent issues""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Finds ticket, shows status, explains next steps, offers to escalate if needed +4 - Good: Provides ticket status and explanation +3 - Adequate: Finds and reports ticket status +2 - Poor: Generic response without checking tickets +1 - Fail: Doesn't help with ticket status""", + ), + + Scenario( + id="support_new_ticket", + name="Create New Support Ticket", + description="Customer wanting to open a new support ticket for equipment issue", + customer_id=14, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 14. My cable box keeps rebooting randomly. I need someone to look at this.", + expected_tool_calls=["get_subscription_detail", "get_support_tickets"], + expected_keywords=["ticket", "equipment", "technician"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_support_tickets"], + success_keywords=["ticket", "equipment", "technician", "issue", "scheduled"], + expected_resolution="Document issue, create support ticket, offer technician visit", + ground_truth_solution="""Handle equipment issue and create ticket. +Key actions: +1. Document the cable box issue (random reboots) +2. Check subscription for equipment details +3. Basic troubleshooting: unplug for 30 seconds, check connections +4. If issue persists, create support ticket + +Support ticket should include: +- Equipment type and issue description +- Troubleshooting steps already attempted +- Priority level based on severity +- Offer technician visit if needed""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Documents issue, tries troubleshooting, creates ticket, offers technician +4 - Good: Creates ticket and offers resolution options +3 - Adequate: Acknowledges issue and offers to help +2 - Poor: Generic troubleshooting without creating ticket +1 - Fail: Doesn't address the equipment issue""", + ), + + # ═══════════════════════════════════════════════════════════════════════════════ + # MULTI-TURN SCENARIOS (5 scenarios with 2-4 turns each) + # ═══════════════════════════════════════════════════════════════════════════════ + Scenario( + id="multi_billing_dispute", + name="[Multi-Turn] Billing Dispute Resolution", + description="Customer disputes charge, agent investigates, customer asks for credit, then upgrade", + customer_id=7, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 7. There's a $50 charge on my bill I don't recognize. What is this for?", + expected_tool_calls=["get_billing_summary"], + expected_keywords=["charge", "invoice", "billing"], + ), + ConversationTurn( + user_message="I didn't order any equipment or additional services. Can you remove this charge?", + expected_tool_calls=[], + expected_keywords=["credit", "remove", "adjustment"], + ), + ConversationTurn( + user_message="Thanks for the credit. While I have you, are there any promotions I qualify for?", + expected_tool_calls=["get_customer_detail"], + expected_keywords=["promotion", "discount", "offer"], + ), + ], + expected_tools=["get_customer_detail", "get_billing_summary", "get_subscription_detail"], + success_keywords=["charge", "credit", "adjustment", "promotion", "discount"], + expected_resolution="Investigate charge, apply credit if warranted, then check promotion eligibility", + ground_truth_solution="""Multi-turn billing dispute resolution: + +Turn 1 - Investigate the charge: +- Pull up billing summary to identify the $50 charge +- Explain what the charge is for (equipment fee, one-time charge, etc.) +- Show when it was applied + +Turn 2 - Handle credit request: +- If charge is erroneous, apply credit +- If valid, explain why but offer goodwill credit if appropriate +- Confirm the adjustment will appear on next bill + +Turn 3 - Check promotions: +- Review customer loyalty level and current services +- Identify applicable promotions +- Recommend best options based on their profile""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Investigates charge thoroughly, handles credit appropriately, provides personalized promotion info +4 - Good: Addresses each turn adequately with relevant information +3 - Adequate: Responds to each turn but missing depth or personalization +2 - Poor: Misses context between turns or provides generic responses +1 - Fail: Fails to address the dispute or loses conversation context""", + ), + + Scenario( + id="multi_internet_troubleshoot", + name="[Multi-Turn] Internet Troubleshooting Flow", + description="Step-by-step troubleshooting: check status, try fixes, escalate to technician", + customer_id=16, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 16. My internet keeps dropping every few minutes. It's really frustrating.", + expected_tool_calls=["get_subscription_detail", "get_support_tickets"], + expected_keywords=["internet", "issue", "connection"], + ), + ConversationTurn( + user_message="I already tried restarting the router. It worked for a bit but started dropping again.", + expected_tool_calls=["search_knowledge"], + expected_keywords=["troubleshoot", "check", "cable"], + ), + ConversationTurn( + user_message="I checked the cables and they look fine. I think there might be something wrong with the equipment.", + expected_tool_calls=[], + expected_keywords=["technician", "appointment", "visit"], + ), + ConversationTurn( + user_message="Yes, please schedule a technician. What times are available?", + expected_tool_calls=[], + expected_keywords=["scheduled", "appointment", "confirm"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge"], + success_keywords=["troubleshoot", "router", "cables", "technician", "appointment", "scheduled"], + expected_resolution="Progressive troubleshooting leading to technician scheduling", + ground_truth_solution="""Multi-turn troubleshooting flow: + +Turn 1 - Initial diagnosis: +- Check subscription and service status +- Look for existing incidents or tickets +- Acknowledge the issue and express empathy + +Turn 2 - Continue troubleshooting: +- Since router restart was tried, suggest next steps +- Check cable connections +- Try wired connection to isolate WiFi vs line issue +- Check for interference + +Turn 3 - Escalate to technician: +- Acknowledge customer has tried basic troubleshooting +- Agree equipment may need inspection +- Offer to schedule technician visit + +Turn 4 - Schedule appointment: +- Offer available time slots +- Confirm appointment details +- Provide technician arrival window +- Mention what technician will check""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Progressive troubleshooting, builds on previous turns, smooth escalation to technician +4 - Good: Addresses each step appropriately, schedules technician +3 - Adequate: Follows the flow but may skip steps or lack continuity +2 - Poor: Repetitive suggestions or doesn't build on previous attempts +1 - Fail: Doesn't progress logically or fails to schedule technician""", + ), + + Scenario( + id="multi_service_cancellation", + name="[Multi-Turn] Service Cancellation Retention", + description="Customer wants to cancel, agent attempts retention with offers", + customer_id=24, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 24. I want to cancel my internet service. It's too expensive.", + expected_tool_calls=["get_subscription_detail", "get_billing_summary"], + expected_keywords=["cancel", "service", "understand"], + ), + ConversationTurn( + user_message="I've been paying $119 a month and I found a competitor offering $70 for similar speeds.", + expected_tool_calls=["get_products"], + expected_keywords=["offer", "discount", "match", "retention"], + ), + ConversationTurn( + user_message="A 20% discount sounds good. What would my new monthly rate be?", + expected_tool_calls=[], + expected_keywords=["$95", "monthly", "rate", "discount"], + ), + ], + expected_tools=["get_customer_detail", "get_subscription_detail", "get_billing_summary", "get_products"], + success_keywords=["cancel", "retention", "discount", "offer", "rate", "save"], + expected_resolution="Understand cancellation reason, offer retention discount, retain customer", + ground_truth_solution="""Multi-turn retention flow: + +Turn 1 - Understand cancellation reason: +- Pull up subscription details and billing +- Express understanding about cost concerns +- Ask about their specific needs +- Don't immediately accept cancellation + +Turn 2 - Make retention offer: +- Acknowledge competitor pricing +- Check for available retention offers +- Offer 20% loyalty discount or price match +- Highlight value-adds (speed, reliability, support) + +Turn 3 - Close the retention: +- Calculate new rate with discount ($119 × 0.8 = $95.20) +- Confirm the discount will be applied +- Explain discount duration (12 months, etc.) +- Thank customer for staying""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Empathetic handling, competitive retention offer, calculates new rate, secures retention +4 - Good: Makes appropriate retention offer and calculates savings +3 - Adequate: Attempts retention but may miss personalization or calculation +2 - Poor: Too quick to cancel or weak retention attempt +1 - Fail: Processes cancellation without retention effort""", + ), + + Scenario( + id="multi_new_customer_setup", + name="[Multi-Turn] New Service Setup Assistance", + description="Customer needs help choosing and setting up new services", + customer_id=2, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 2. I just moved to a new apartment and need to set up internet. What are my options?", + expected_tool_calls=["get_products"], + expected_keywords=["internet", "plans", "options"], + ), + ConversationTurn( + user_message="I work from home and need reliable internet for video calls. Which plan do you recommend?", + expected_tool_calls=["get_subscription_detail"], + expected_keywords=["Pro", "500Mbps", "recommend"], + ), + ConversationTurn( + user_message="The Pro plan sounds good. Do you have any current promotions for new setups?", + expected_tool_calls=[], + expected_keywords=["promotion", "discount", "new customer"], + ), + ConversationTurn( + user_message="Great! Please set me up with the Pro plan and the new customer discount.", + expected_tool_calls=[], + expected_keywords=["confirm", "order", "setup", "welcome"], + ), + ], + expected_tools=["get_customer_detail", "get_products", "get_subscription_detail"], + success_keywords=["internet", "Pro", "500Mbps", "promotion", "discount", "setup", "order"], + expected_resolution="Guide through plan selection, apply promotion, complete setup", + ground_truth_solution="""Multi-turn new customer setup: + +Turn 1 - Present options: +- List available internet plans (Basic, Pro, Ultimate) +- Explain speed tiers and pricing +- Ask about usage needs + +Turn 2 - Make recommendation: +- For WFH with video calls, recommend Pro (500 Mbps) +- Explain why it's suitable (consistent speed, WiFi 6, priority support) +- Mention Ultimate if they want overkill + +Turn 3 - Present promotions: +- New Customer 20% off first 3 months +- Mention WiFi 6 router included +- Explain installation options + +Turn 4 - Complete setup: +- Confirm plan selection (Pro @ $79.99) +- Apply 20% promotion (first 3 months = $63.99) +- Set installation date +- Welcome to Contoso""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Natural sales flow, personalized recommendation, applies promo, completes setup smoothly +4 - Good: Guides through selection and setup with appropriate recommendations +3 - Adequate: Completes setup but may lack personalization or miss promotion +2 - Poor: Disjointed experience or missing key steps +1 - Fail: Doesn't complete the setup or loses track of conversation""", + ), + + Scenario( + id="multi_complex_account_issue", + name="[Multi-Turn] Complex Account Resolution", + description="Customer has multiple issues: wrong charge, slow internet, and needs plan change", + customer_id=11, + turns=[ + ConversationTurn( + user_message="Hi, I'm customer 11. I have several issues. First, I was charged for a service I cancelled last month.", + expected_tool_calls=["get_billing_summary", "get_subscription_detail"], + expected_keywords=["charge", "cancelled", "billing"], + ), + ConversationTurn( + user_message="Also, my internet has been slow for the past week. Are there any known issues?", + expected_tool_calls=["get_support_tickets"], + expected_keywords=["slow", "internet", "incident", "issue"], + ), + ConversationTurn( + user_message="One more thing - I want to downgrade my TV package. I don't watch that much anymore.", + expected_tool_calls=["get_products"], + expected_keywords=["downgrade", "TV", "package", "change"], + ), + ConversationTurn( + user_message="Can you summarize all the changes you're making to my account?", + expected_tool_calls=[], + expected_keywords=["summary", "credit", "downgrade", "changes"], + ), + ], + expected_tools=["get_customer_detail", "get_billing_summary", "get_subscription_detail", "get_support_tickets", "get_products"], + success_keywords=["credit", "refund", "slow", "incident", "downgrade", "TV", "summary", "changes"], + expected_resolution="Handle billing credit, check internet issue, process TV downgrade, summarize all changes", + ground_truth_solution="""Multi-turn complex account resolution: + +Turn 1 - Billing issue: +- Check billing for the cancelled service charge +- Identify the erroneous charge +- Apply credit/refund for the amount +- Confirm it will be removed + +Turn 2 - Internet issue: +- Check for service incidents +- Check subscription service status +- If incident exists, provide status and ETA +- If not, offer troubleshooting + +Turn 3 - TV downgrade: +- Show current TV package +- Explain downgrade options (Premium to Basic) +- Calculate savings +- Process the change + +Turn 4 - Summary: +- Recap all changes made: + 1. Credit applied for erroneous charge: $X + 2. Internet issue: status/resolution + 3. TV downgrade: from Premium to Basic, saving $X/month +- Confirm customer is satisfied""", + scoring_rubric="""Score 1-5 based on these criteria: +5 - Excellent: Handles all 3 issues effectively, provides clear summary, maintains context throughout +4 - Good: Addresses all issues with reasonable resolution +3 - Adequate: Handles most issues but may miss one or lack cohesive summary +2 - Poor: Loses track of issues or provides incomplete resolution +1 - Fail: Unable to handle multiple issues or forgets earlier requests""", + ), +] + + +# ═══════════════════════════════════════════════════════════════════════════════ +# SCENARIO EVALUATOR (Using AgentTestRunner) +# ═══════════════════════════════════════════════════════════════════════════════ + + +@dataclass +class ScenarioResult: + """Complete result from running a scenario.""" + scenario: Scenario + agent_name: str + query_results: list[QueryResult] = field(default_factory=list) + total_time: float = 0.0 + + # Tool-based metrics (Process Evaluation) - Rule-based F1 + tool_recall: float = 0.0 # % of expected tools called + tool_precision: float = 0.0 # % of called tools that were expected + tool_f1: float = 0.0 # Harmonic mean of precision and recall + + # Outcome-based metrics (Goal Evaluation) - Keyword matching fallback + keyword_coverage: float = 0.0 # % of success keywords in response + response_length: int = 0 # Total response length + + # LLM-as-Judge metrics (Azure AI Foundry) + llm_intent_score: Optional[float] = None # 1-5: Did agent understand intent? + llm_intent_result: Optional[str] = None # "pass" or "fail" + llm_intent_reason: Optional[str] = None + llm_task_score: Optional[float] = None # 1-5: Did response follow task? + llm_task_result: Optional[str] = None + llm_task_reason: Optional[str] = None + llm_tool_score: Optional[float] = None # 1-5: Were correct tools called? + llm_tool_result: Optional[str] = None + llm_coherence: Optional[float] = None # 1-5: Is response coherent? + llm_fluency: Optional[float] = None # 1-5: Is language natural? + llm_relevance: Optional[float] = None # 1-5: Is response relevant? + llm_solution_score: Optional[float] = None # 1-5: Solution accuracy vs ground truth + llm_solution_reason: Optional[str] = None # Explanation of solution score + llm_eval_time: float = 0.0 + llm_errors: list[str] = field(default_factory=list) + + # Overall + success: bool = False + + def compute_metrics(self): + """Compute both tool accuracy and outcome metrics (rule-based).""" + # Collect all tools and responses + all_tools_called = set() + all_responses = [] + + for qr in self.query_results: + all_tools_called.update(qr.tool_calls) + all_responses.append(qr.response.lower()) + + combined_response = " ".join(all_responses) + self.response_length = len(combined_response) + + # ───────────────────────────────────────────────────────────────────── + # TOOL ACCURACY (Process-Based) - Rule-based F1 + # ───────────────────────────────────────────────────────────────────── + expected_tools = set(self.scenario.expected_tools) + + if expected_tools: + # Recall: What % of expected tools were called? + self.tool_recall = len(all_tools_called & expected_tools) / len(expected_tools) + + if all_tools_called: + # Precision: What % of called tools were expected? + self.tool_precision = len(all_tools_called & expected_tools) / len(all_tools_called) + + # F1 Score: Harmonic mean + if self.tool_precision + self.tool_recall > 0: + self.tool_f1 = 2 * (self.tool_precision * self.tool_recall) / (self.tool_precision + self.tool_recall) + + # ───────────────────────────────────────────────────────────────────── + # KEYWORD COVERAGE (Outcome-Based) - Fallback when LLM judge not used + # ───────────────────────────────────────────────────────────────────── + if self.scenario.success_keywords: + found = sum(1 for kw in self.scenario.success_keywords if kw.lower() in combined_response) + self.keyword_coverage = found / len(self.scenario.success_keywords) + + # ───────────────────────────────────────────────────────────────────── + # SUCCESS CRITERIA + # ───────────────────────────────────────────────────────────────────── + # If LLM judge was used, use its results + if self.llm_intent_result is not None or self.llm_task_result is not None: + # LLM-based success: intent resolved or task adhered + llm_passes = [] + if self.llm_intent_result: + llm_passes.append(self.llm_intent_result == "pass") + if self.llm_task_result: + llm_passes.append(self.llm_task_result == "pass") + self.success = any(llm_passes) if llm_passes else self.keyword_coverage >= 0.5 + else: + # Keyword-based success + has_no_errors = all(qr.error is None for qr in self.query_results) + self.success = self.keyword_coverage >= 0.5 and has_no_errors + + async def compute_llm_metrics(self, evaluator: "LLMJudgeEvaluator"): + """Compute LLM-as-Judge metrics using Azure AI Foundry evaluators.""" + if not self.query_results: + return + + # Get the query and response + query = self.scenario.turns[0].user_message if self.scenario.turns else "" + response = self.query_results[-1].response if self.query_results else "" + + # Get tool calls from all turns + all_tool_calls = [] + for qr in self.query_results: + for tool_name in qr.tool_calls: + all_tool_calls.append(ToolCall(name=tool_name)) + + # Convert MCP tool definitions + tool_defs = [ + ToolDefinition(name=td["name"], description=td["description"]) + for td in MCP_TOOL_DEFINITIONS + ] + + # Run LLM evaluation + try: + result = await evaluator.evaluate( + query=query, + response=response, + tool_calls=all_tool_calls, + tool_definitions=tool_defs, + ground_truth_solution=self.scenario.ground_truth_solution, + scoring_rubric=self.scenario.scoring_rubric, + ) + + # Copy results + self.llm_intent_score = result.intent_resolution_score + self.llm_intent_result = result.intent_resolution_result + self.llm_intent_reason = result.intent_resolution_reason + self.llm_task_score = result.task_adherence_score + self.llm_task_result = result.task_adherence_result + self.llm_task_reason = result.task_adherence_reason + self.llm_tool_score = result.tool_call_accuracy_score + self.llm_tool_result = result.tool_call_accuracy_result + self.llm_coherence = result.coherence_score + self.llm_fluency = result.fluency_score + self.llm_relevance = result.relevance_score + self.llm_solution_score = result.solution_accuracy_score + self.llm_solution_reason = result.solution_accuracy_reason + self.llm_eval_time = result.evaluation_time + self.llm_errors = result.errors + + except Exception as e: + self.llm_errors.append(f"LLM evaluation failed: {e}") + + +class ScenarioEvaluator: + """ + Runs scenarios against any agent using the generic AgentTestRunner. + Evaluates both tool accuracy and outcome quality. + + Supports two evaluation modes: + 1. Rule-based (default): Tool F1 + keyword matching + 2. LLM-as-Judge: Azure AI Foundry evaluators (IntentResolution, TaskAdherence, etc.) + """ + + def __init__( + self, + agent_name: str = "single", + use_llm_judge: bool = None, + enable_quality_metrics: bool = True, + ): + """ + Args: + agent_name: Agent shorthand ("single", "reflection", "handoff", "magentic") + or full module path + use_llm_judge: Use LLM-as-Judge evaluators (default: from env var) + enable_quality_metrics: Include coherence, fluency, relevance (slower) + """ + self.agent_name = agent_name + self.runner = AgentTestRunner(agent_name) + + # Determine if LLM judge should be used + if use_llm_judge is None: + use_llm_judge = USE_LLM_JUDGE and LLM_JUDGE_AVAILABLE + + self.use_llm_judge = use_llm_judge + self.enable_quality_metrics = enable_quality_metrics + self.llm_evaluator = None + + if self.use_llm_judge and LLM_JUDGE_AVAILABLE: + self.llm_evaluator = LLMJudgeEvaluator( + enable_agent_evaluators=True, + enable_quality_evaluators=enable_quality_metrics, + ) + print(f" [LLM] LLM-as-Judge: ENABLED") + else: + print(f" [RULE] LLM-as-Judge: DISABLED (using keyword matching)") + + async def run_scenario(self, scenario: Scenario) -> ScenarioResult: + """Run a complete scenario and return results.""" + result = ScenarioResult(scenario=scenario, agent_name=self.agent_name) + + start_time = time.time() + + # Run each turn in the scenario + for turn in scenario.turns: + query_result = await self.runner.run_query(turn.user_message) + result.query_results.append(query_result) + + result.total_time = time.time() - start_time + + # Compute rule-based metrics first + result.compute_metrics() + + # Optionally run LLM judge evaluation + if self.llm_evaluator is not None: + await result.compute_llm_metrics(self.llm_evaluator) + # Recompute success based on LLM results + result.compute_metrics() + + return result + + async def run_all_scenarios( + self, + scenarios: list[Scenario] | None = None, + verbose: bool = True, + ) -> list[ScenarioResult]: + """Run all scenarios and return results.""" + scenarios = scenarios or SCENARIOS + results = [] + + for scenario in scenarios: + if verbose: + print(f"\n[{self.agent_name}] Running: {scenario.name}") + + result = await self.run_scenario(scenario) + results.append(result) + + if verbose: + status = "✅" if result.success else "❌" + + # Show different metrics based on evaluation mode + if result.llm_intent_score is not None: + # LLM judge mode + intent = f"Intent: {result.llm_intent_score:.0f}/5" if result.llm_intent_score else "Intent: N/A" + task = f"Task: {result.llm_task_score:.0f}/5" if result.llm_task_score else "Task: N/A" + print(f" {status} {intent}, {task}, Time: {result.total_time:.1f}s (+{result.llm_eval_time:.1f}s eval)") + else: + # Keyword mode + print(f" {status} Tool F1: {result.tool_f1:.1%}, " + f"Keywords: {result.keyword_coverage:.1%}, " + f"Time: {result.total_time:.1f}s") + + return results + + +# ═══════════════════════════════════════════════════════════════════════════════ +# COMPARISON REPORT +# ═══════════════════════════════════════════════════════════════════════════════ + + +@dataclass +class AgentSummary: + """Aggregated metrics for an agent across all scenarios.""" + agent_name: str + scenarios_passed: int = 0 + scenarios_total: int = 0 + + # Rule-based metrics + avg_tool_recall: float = 0.0 + avg_tool_precision: float = 0.0 + avg_tool_f1: float = 0.0 + avg_keyword_coverage: float = 0.0 + + # LLM-as-Judge metrics + avg_intent_score: Optional[float] = None + avg_task_score: Optional[float] = None + avg_tool_score: Optional[float] = None + avg_coherence: Optional[float] = None + avg_fluency: Optional[float] = None + avg_relevance: Optional[float] = None + avg_solution_score: Optional[float] = None # Solution accuracy vs ground truth + intent_pass_rate: float = 0.0 + task_pass_rate: float = 0.0 + solution_pass_rate: float = 0.0 # % with solution score >= 3 + + avg_time: float = 0.0 + total_tools_called: int = 0 + uses_llm_judge: bool = False + + @classmethod + def from_results(cls, agent_name: str, results: list[ScenarioResult]) -> "AgentSummary": + if not results: + return cls(agent_name=agent_name) + + n = len(results) + + # Check if LLM judge was used + has_llm = any(r.llm_intent_score is not None for r in results) + + summary = cls( + agent_name=agent_name, + scenarios_passed=sum(1 for r in results if r.success), + scenarios_total=n, + avg_tool_recall=sum(r.tool_recall for r in results) / n, + avg_tool_precision=sum(r.tool_precision for r in results) / n, + avg_tool_f1=sum(r.tool_f1 for r in results) / n, + avg_keyword_coverage=sum(r.keyword_coverage for r in results) / n, + avg_time=sum(r.total_time for r in results) / n, + total_tools_called=sum(len(qr.tool_calls) for r in results for qr in r.query_results), + uses_llm_judge=has_llm, + ) + + # Compute LLM judge averages if available + if has_llm: + intent_scores = [r.llm_intent_score for r in results if r.llm_intent_score is not None] + task_scores = [r.llm_task_score for r in results if r.llm_task_score is not None] + tool_scores = [r.llm_tool_score for r in results if r.llm_tool_score is not None] + coherence = [r.llm_coherence for r in results if r.llm_coherence is not None] + fluency = [r.llm_fluency for r in results if r.llm_fluency is not None] + relevance = [r.llm_relevance for r in results if r.llm_relevance is not None] + solution_scores = [r.llm_solution_score for r in results if r.llm_solution_score is not None] + + if intent_scores: + summary.avg_intent_score = sum(intent_scores) / len(intent_scores) + if task_scores: + summary.avg_task_score = sum(task_scores) / len(task_scores) + if tool_scores: + summary.avg_tool_score = sum(tool_scores) / len(tool_scores) + if coherence: + summary.avg_coherence = sum(coherence) / len(coherence) + if fluency: + summary.avg_fluency = sum(fluency) / len(fluency) + if relevance: + summary.avg_relevance = sum(relevance) / len(relevance) + if solution_scores: + summary.avg_solution_score = sum(solution_scores) / len(solution_scores) + # Pass rate: score >= 3 (Adequate or better) + summary.solution_pass_rate = sum(1 for s in solution_scores if s >= 3) / len(solution_scores) + + # Pass rates + intent_passes = [r for r in results if r.llm_intent_result == "pass"] + task_passes = [r for r in results if r.llm_task_result == "pass"] + summary.intent_pass_rate = len(intent_passes) / n + summary.task_pass_rate = len(task_passes) / n + + return summary + + +def generate_comparison_report( + results_by_agent: dict[str, list[ScenarioResult]], +) -> str: + """Generate a comprehensive comparison report.""" + + # Check if LLM judge was used + first_results = list(results_by_agent.values())[0] + uses_llm = any(r.llm_intent_score is not None for r in first_results) + + mode = "LLM-as-Judge (Azure AI Foundry)" if uses_llm else "Rule-Based (Tool F1 + Keywords)" + + lines = [ + "", + "═" * 90, + f"AGENT EVALUATION REPORT: {mode}", + "═" * 90, + ] + + # Get agent names + agent_names = list(results_by_agent.keys()) + + # Per-scenario breakdown + lines.extend([ + "", + "SCENARIO BREAKDOWN", + "-" * 90, + ]) + + if uses_llm: + lines.append(f"{'Scenario':<28} {'Agent':<12} {'Pass':<6} {'Intent':<8} {'Solution':<10} {'Time':<8}") + else: + lines.append(f"{'Scenario':<28} {'Agent':<12} {'Pass':<6} {'Tool F1':<10} {'Keywords':<10} {'Time':<8}") + + lines.append("-" * 90) + + # Assume all agents ran same scenarios in same order + first_agent_results = results_by_agent[agent_names[0]] + + for i, scenario in enumerate([r.scenario for r in first_agent_results]): + scenario_name = scenario.name[:26] + + for agent_name in agent_names: + result = results_by_agent[agent_name][i] + status = "✅" if result.success else "❌" + + if uses_llm: + intent = f"{result.llm_intent_score:.0f}/5" if result.llm_intent_score else "N/A" + solution = f"{result.llm_solution_score:.0f}/5" if result.llm_solution_score else "N/A" + lines.append( + f"{scenario_name:<28} {agent_name:<12} {status:<6} " + f"{intent:<8} {solution:<10} {result.total_time:>6.1f}s" + ) + else: + lines.append( + f"{scenario_name:<28} {agent_name:<12} {status:<6} " + f"{result.tool_f1:>8.1%} {result.keyword_coverage:>10.1%} " + f"{result.total_time:>6.1f}s" + ) + + lines.append("") # Space between scenarios + + # Summary statistics + lines.extend([ + "-" * 90, + "SUMMARY", + "-" * 90, + "", + ]) + + # Header with agent names + header = f"{'Metric':<30}" + for name in agent_names: + header += f" {name:>15}" + lines.append(header) + lines.append("-" * (30 + 16 * len(agent_names))) + + # Compute summaries + summaries = {name: AgentSummary.from_results(name, results) + for name, results in results_by_agent.items()} + + # Metrics rows - different based on mode + if uses_llm: + metrics = [ + ("Scenarios Passed", lambda s: f"{s.scenarios_passed}/{s.scenarios_total}"), + ("Solution Pass Rate (>=3)", lambda s: f"{s.solution_pass_rate:.1%}" if s.avg_solution_score else "N/A"), + ("Avg Solution Score", lambda s: f"{s.avg_solution_score:.1f}/5" if s.avg_solution_score else "N/A"), + ("Avg Intent Score", lambda s: f"{s.avg_intent_score:.1f}/5" if s.avg_intent_score else "N/A"), + ("Avg Coherence", lambda s: f"{s.avg_coherence:.1f}/5" if s.avg_coherence else "N/A"), + ("Avg Fluency", lambda s: f"{s.avg_fluency:.1f}/5" if s.avg_fluency else "N/A"), + ("Avg Relevance", lambda s: f"{s.avg_relevance:.1f}/5" if s.avg_relevance else "N/A"), + ("Avg Time (s)", lambda s: f"{s.avg_time:.1f}"), + ("Total Tools Called", lambda s: f"{s.total_tools_called}"), + ] + else: + metrics = [ + ("Scenarios Passed", lambda s: f"{s.scenarios_passed}/{s.scenarios_total}"), + ("Avg Tool Recall", lambda s: f"{s.avg_tool_recall:.1%}"), + ("Avg Tool Precision", lambda s: f"{s.avg_tool_precision:.1%}"), + ("Avg Tool F1", lambda s: f"{s.avg_tool_f1:.1%}"), + ("Avg Keyword Coverage", lambda s: f"{s.avg_keyword_coverage:.1%}"), + ("Avg Time (s)", lambda s: f"{s.avg_time:.1f}"), + ("Total Tools Called", lambda s: f"{s.total_tools_called}"), + ] + + for metric_name, formatter in metrics: + row = f"{metric_name:<30}" + for name in agent_names: + row += f" {formatter(summaries[name]):>15}" + lines.append(row) + + lines.extend(["", "═" * 90, ""]) + + return "\n".join(lines) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# PYTEST TESTS +# ═══════════════════════════════════════════════════════════════════════════════ + + +@pytest.fixture +def single_evaluator(): + return ScenarioEvaluator(agent_name="single") + + +@pytest.fixture +def reflection_evaluator(): + return ScenarioEvaluator(agent_name="reflection") + + +class TestScenarioEvaluation: + """Scenario-based evaluation tests.""" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_single_scenario_billing(self, single_evaluator): + """Test single agent on billing scenario.""" + scenario = SCENARIOS[0] # billing_high_invoice + result = await single_evaluator.run_scenario(scenario) + + print(f"\nScenario: {scenario.name}") + print(f"Response: {result.query_results[0].response[:300]}...") + print(f"Tools called: {result.query_results[0].tool_calls}") + print(f"Tool F1: {result.tool_f1:.1%}") + print(f"Keyword coverage: {result.keyword_coverage:.1%}") + + assert result.keyword_coverage >= 0.3, "Should mention some relevant keywords" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_all_scenarios_single_agent(self, single_evaluator): + """Run all scenarios with single agent.""" + results = await single_evaluator.run_all_scenarios() + + passed = sum(1 for r in results if r.success) + print(f"\nSingle Agent: {passed}/{len(results)} scenarios passed") + + assert passed >= len(results) // 2, "At least half of scenarios should pass" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_all_scenarios_reflection_agent(self, reflection_evaluator): + """Run all scenarios with reflection agent.""" + results = await reflection_evaluator.run_all_scenarios() + + passed = sum(1 for r in results if r.success) + print(f"\nReflection Agent: {passed}/{len(results)} scenarios passed") + + assert passed >= len(results) // 2, "At least half of scenarios should pass" + + +class TestAgentComparison: + """Compare multiple agents on all scenarios.""" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_compare_single_vs_reflection(self): + """Compare single vs reflection agent with full metrics.""" + agents = ["single", "reflection"] + results_by_agent: dict[str, list[ScenarioResult]] = {} + + for agent_name in agents: + print(f"\n{'=' * 40}") + print(f"Running {agent_name} agent...") + print("=" * 40) + + evaluator = ScenarioEvaluator(agent_name=agent_name) + results = await evaluator.run_all_scenarios() + results_by_agent[agent_name] = results + + # Generate and print report + report = generate_comparison_report(results_by_agent) + print(report) + + # Save results + results_file = _eval_dir / "agent_comparison_results.json" + _save_results(results_by_agent, results_file) + + print(f"\nResults saved to: {results_file}") + + # Basic assertions + for agent_name, results in results_by_agent.items(): + passed = sum(1 for r in results if r.success) + assert passed >= 1, f"{agent_name} should pass at least 1 scenario" + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_compare_all_agents_parallel(self): + """Compare ALL agents in parallel for speed.""" + # Available agents: single, reflection, handoff, magentic + # Note: magentic excluded due to tool call tracking issues + agents = ["single", "reflection", "handoff"] + + async def run_agent(agent_name: str) -> tuple[str, list[ScenarioResult]]: + """Run single agent evaluation.""" + print(f"\n[{agent_name}] Starting evaluation...") + evaluator = ScenarioEvaluator(agent_name=agent_name) + results = await evaluator.run_all_scenarios() + passed = sum(1 for r in results if r.success) + print(f"[{agent_name}] Completed: {passed}/{len(results)} passed") + return agent_name, results + + # Run all agents in parallel + print("\n" + "=" * 60) + print("RUNNING ALL AGENTS IN PARALLEL") + print("=" * 60) + + import time + start_time = time.time() + + # Execute all agents concurrently + tasks = [run_agent(agent_name) for agent_name in agents] + agent_results = await asyncio.gather(*tasks, return_exceptions=True) + + total_time = time.time() - start_time + + # Collect results (filter out exceptions) + results_by_agent: dict[str, list[ScenarioResult]] = {} + for result in agent_results: + if isinstance(result, Exception): + print(f"Agent failed with error: {result}") + else: + agent_name, results = result + results_by_agent[agent_name] = results + + # Generate and print report + if results_by_agent: + report = generate_comparison_report(results_by_agent) + print(report) + + print(f"\nTotal parallel execution time: {total_time:.1f}s") + print(f"(Sequential would be ~{total_time * len(agents):.1f}s)") + + # Save results + results_file = _eval_dir / "all_agents_comparison.json" + _save_results(results_by_agent, results_file) + print(f"\nResults saved to: {results_file}") + + # Basic assertions + assert len(results_by_agent) >= 2, "At least 2 agents should complete" + for agent_name, results in results_by_agent.items(): + passed = sum(1 for r in results if r.success) + assert passed >= 1, f"{agent_name} should pass at least 1 scenario" + + +def _save_results(results_by_agent: dict[str, list[ScenarioResult]], results_file: Path): + """Save evaluation results to JSON file.""" + with open(results_file, "w") as f: + json.dump({ + agent_name: [ + { + "scenario": r.scenario.id, + "scenario_name": r.scenario.name, + "success": r.success, + # Rule-based metrics + "tool_recall": r.tool_recall, + "tool_precision": r.tool_precision, + "tool_f1": r.tool_f1, + "keyword_coverage": r.keyword_coverage, + "total_time": r.total_time, + "tools_called": [tc for qr in r.query_results for tc in qr.tool_calls], + # LLM-as-Judge metrics + "llm_intent_score": r.llm_intent_score, + "llm_intent_result": r.llm_intent_result, + "llm_intent_reason": r.llm_intent_reason, + "llm_task_score": r.llm_task_score, + "llm_task_result": r.llm_task_result, + "llm_task_reason": r.llm_task_reason, + "llm_tool_score": r.llm_tool_score, + "llm_coherence": r.llm_coherence, + "llm_fluency": r.llm_fluency, + "llm_relevance": r.llm_relevance, + "llm_solution_score": r.llm_solution_score, + "llm_solution_reason": r.llm_solution_reason, + "llm_eval_time": r.llm_eval_time, + } + for r in results + ] + for agent_name, results in results_by_agent.items() + }, f, indent=2) + + +# ═══════════════════════════════════════════════════════════════════════════════ +# STANDALONE EXECUTION +# ═══════════════════════════════════════════════════════════════════════════════ + +if __name__ == "__main__": + import logging + import warnings + + # Suppress MCP client cleanup warnings + logging.getLogger("asyncio").setLevel(logging.CRITICAL) + warnings.filterwarnings("ignore", category=DeprecationWarning) + + async def main(): + print("Agent Evaluation: Tool Accuracy + Outcome Quality") + print("=" * 60) + + agents = ["single", "reflection"] + results_by_agent: dict[str, list[ScenarioResult]] = {} + + for agent_name in agents: + print(f"\n{'─' * 40}") + print(f"Running {agent_name} agent on {len(SCENARIOS)} scenarios...") + print("─" * 40) + + evaluator = ScenarioEvaluator(agent_name=agent_name) + results = await evaluator.run_all_scenarios() + results_by_agent[agent_name] = results + + print(generate_comparison_report(results_by_agent)) + + asyncio.run(main()) diff --git a/tests/evaluation/uv.lock b/tests/evaluation/uv.lock new file mode 100644 index 000000000..412ec574a --- /dev/null +++ b/tests/evaluation/uv.lock @@ -0,0 +1,3198 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version < '3.13'", +] + +[options] +prerelease-mode = "allow" + +[[package]] +name = "a2a-sdk" +version = "0.3.22" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "protobuf" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/a3/76f2d94a32a1b0dc760432d893a09ec5ed31de5ad51b1ef0f9d199ceb260/a2a_sdk-0.3.22.tar.gz", hash = "sha256:77a5694bfc4f26679c11b70c7f1062522206d430b34bc1215cfbb1eba67b7e7d", size = 231535 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/e8/f4e39fd1cf0b3c4537b974637143f3ebfe1158dad7232d9eef15666a81ba/a2a_sdk-0.3.22-py3-none-any.whl", hash = "sha256:b98701135bb90b0ff85d35f31533b6b7a299bf810658c1c65f3814a6c15ea385", size = 144347 }, +] + +[[package]] +name = "ag-ui-protocol" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889 }, +] + +[[package]] +name = "agent-evaluation" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "agent-framework" }, + { name = "azure-ai-evaluation" }, + { name = "azure-identity" }, + { name = "httpx" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-timeout" }, + { name = "python-dotenv" }, +] + +[package.metadata] +requires-dist = [ + { name = "agent-framework", specifier = "==1.0.0b260107" }, + { name = "azure-ai-evaluation", specifier = ">=1.0.0" }, + { name = "azure-identity", specifier = ">=1.15.0" }, + { name = "httpx", specifier = ">=0.27.0" }, + { name = "openai", specifier = ">=2.5.0" }, + { name = "pydantic", specifier = ">=2.0.0" }, + { name = "pytest", specifier = ">=8.0.0" }, + { name = "pytest-asyncio", specifier = ">=0.23.0" }, + { name = "pytest-timeout", specifier = ">=2.3.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, +] + +[[package]] +name = "agent-framework" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core", extra = ["all"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/e7/5ad52075da4e586ca94fb8806b3085ac5dea8059413e413bff88c0452e88/agent_framework-1.0.0b260107.tar.gz", hash = "sha256:a2f6508a0ca1df3b7ca4e3a64e45bac8e33cdfe02cf69e9056e37e881a58aad7", size = 2898189 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/55/ffef27526cc26bf163ccf9d58ba87bf4e677bba343a542e7b666846f744d/agent_framework-1.0.0b260107-py3-none-any.whl", hash = "sha256:080deb32bff4ef07227a4ba709798c67079ff8a2997fe7a0aed0010adc0c18cf", size = 5554 }, +] + +[[package]] +name = "agent-framework-a2a" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "a2a-sdk" }, + { name = "agent-framework-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/9a/7314f4b4b9b3dffb0ace8681baf0e330a7fd8de55deb09f917024b854b3d/agent_framework_a2a-1.0.0b260107.tar.gz", hash = "sha256:f22f4eff856dd93d32ec07ffc30608ca54308c4fdcc007c028d8616314893b46", size = 7281 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/92/45e37c57427b9613e54fc3ad865cfc4d4784a0287576b55fd6f3f884056b/agent_framework_a2a-1.0.0b260107-py3-none-any.whl", hash = "sha256:e56f9836c6fb5d60b0750a8a1339f0f09cec6e3ea2ef3bf327ea5c10378b7dff", size = 7502 }, +] + +[[package]] +name = "agent-framework-ag-ui" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ag-ui-protocol" }, + { name = "agent-framework-core" }, + { name = "fastapi" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/d5/11fe7cae81192d0ffe816c59ddf0284b947a7a32da3072c99f2bb11e9a5c/agent_framework_ag_ui-1.0.0b260107.tar.gz", hash = "sha256:c0f79f08c3ea2c1a6454fab8cd46a5f94df2e8db71a76b5d7906735087f66349", size = 85637 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/5b/3675630c6ed72213c2309c1b6b92a7b9496e42ca249826625c8cb4e16796/agent_framework_ag_ui-1.0.0b260107-py3-none-any.whl", hash = "sha256:532a34ebbb761cf5511db4ac6b1c5461cf0ee266bf0ccd961f4f8fb9ca5dff5f", size = 62472 }, +] + +[[package]] +name = "agent-framework-anthropic" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "anthropic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/d4/9d002f6333f45d453fc8766b73df0d9fb69e486c678abea017215949e66d/agent_framework_anthropic-1.0.0b260107.tar.gz", hash = "sha256:731d8d16e4a39030e382ae826f0fd123b04a64c4020435ad0ba6290bd461b2f3", size = 9321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/75/daaabe378802a918d7bceb6c52e04b332112c89c819f9eaaa00f1f1f37b0/agent_framework_anthropic-1.0.0b260107-py3-none-any.whl", hash = "sha256:47a4fe893769a15594c663ae2f27132f32cea4393bffe4578a1df49ee70f8a23", size = 9322 }, +] + +[[package]] +name = "agent-framework-azure-ai" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "aiohttp" }, + { name = "azure-ai-agents" }, + { name = "azure-ai-projects" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/26/954d48dbe6e2d558e8425dce1a62238787350f501443081f0de9eab0d9c5/agent_framework_azure_ai-1.0.0b260107.tar.gz", hash = "sha256:bfbec64bf89382833aea18526bb4970b540f9afb269a0eb96bbaed07a3ae6f66", size = 19840 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/c0/ca16e4d772baa2b9d94efebefb5c0d795cddc1428b25a40f4eee7eec8415/agent_framework_azure_ai-1.0.0b260107-py3-none-any.whl", hash = "sha256:001f82bec04d73a8d5e0cf34a9f613963e50db7d46ae000625554306c8271976", size = 21431 }, +] + +[[package]] +name = "agent-framework-azure-ai-search" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "azure-search-documents" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/e6/15f6bb752e900a4262bc2469c3947d7bd85793ebe88b596fa7ea11c0eec5/agent_framework_azure_ai_search-1.0.0b260107.tar.gz", hash = "sha256:1037e1addcab8805f000b0a24725470715fcd758b2a165650a28583dcd30d1b1", size = 13317 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/c9/81379dca1f280222170d6561d63f5ed1f0e2477e51926f081d4e7cd2bb88/agent_framework_azure_ai_search-1.0.0b260107-py3-none-any.whl", hash = "sha256:59dd3e559ca2920b952c4786b4889e060fa7b0f4df1e236c43a82e92142aaa86", size = 13447 }, +] + +[[package]] +name = "agent-framework-azurefunctions" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "azure-functions" }, + { name = "azure-functions-durable" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/74/94a8e1aa0f4264f75c992d76f61fc13f73ba28ecfaabebb132b76a77aa9c/agent_framework_azurefunctions-1.0.0b260107.tar.gz", hash = "sha256:83c22ecd1706593e5223cafd0c348a4cf2d3379d8d06528940e2d77cb66c752e", size = 33705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/b7/e0ac2145d7c7dadca7c7cae03d31f097e9b913c132311fc5e781efe351a4/agent_framework_azurefunctions-1.0.0b260107-py3-none-any.whl", hash = "sha256:97581152a4d4e7a9dad1199e5d748bb77ef63522572d5c6cb9de4717372b2037", size = 37356 }, +] + +[[package]] +name = "agent-framework-chatkit" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "openai-chatkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/8a/c0d1afda3707f9a369be8a235a493ce6c3a645fe87b9ce414dbac97373cd/agent_framework_chatkit-1.0.0b260107.tar.gz", hash = "sha256:9bd46fe9f22acb741c75bde038d738489a518c30dad56b16ad26592598e870f5", size = 12428 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/cd/d7e578239a89977028584dfc8494901cb83824a0f1045369ed55f1dd9c7d/agent_framework_chatkit-1.0.0b260107-py3-none-any.whl", hash = "sha256:88665fd24bafb78b8649d10d267dd27f62cac0b70489044299574288ba8457f3", size = 11726 }, +] + +[[package]] +name = "agent-framework-copilotstudio" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "microsoft-agents-copilotstudio-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/e7/43d3f8b4650b4c4ff214a6340676b7d3bd8087ba280fbbfedc91746bcabf/agent_framework_copilotstudio-1.0.0b260107.tar.gz", hash = "sha256:72d53bd625540786c0989c78e3f57a5941349ec2dc0dfc74c4bd85e0c4e79b47", size = 8525 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/3c/2fbe13fbcc97a4568d34604f1a730af2699b201c50627a918aa02951a680/agent_framework_copilotstudio-1.0.0b260107-py3-none-any.whl", hash = "sha256:dbd5bf97460de6f40cac524f52acd458cb1a1c6c1cac1c8bb3317edf0112fd90", size = 8711 }, +] + +[[package]] +name = "agent-framework-core" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity" }, + { name = "mcp", extra = ["ws"] }, + { name = "openai" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "opentelemetry-semantic-conventions-ai" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/44/06f5d2c99dd7bdb82c2cb5cbc354b5bc6af72d1886d20eff1dff83508fae/agent_framework_core-1.0.0b260107.tar.gz", hash = "sha256:12636fb64664c6153546f0d85dafccdbe57226767c14b3f38985867389f980bb", size = 3574757 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5a/8c6315a2ca119ad48340344616d4b8e77fd68e2892f82c402069a52ad647/agent_framework_core-1.0.0b260107-py3-none-any.whl", hash = "sha256:5bd119b8d30dc2d5bee1c4a5c3597d7afc808a52e4de148725c4f2d9bcc7632b", size = 5687298 }, +] + +[package.optional-dependencies] +all = [ + { name = "agent-framework-a2a" }, + { name = "agent-framework-ag-ui" }, + { name = "agent-framework-anthropic" }, + { name = "agent-framework-azure-ai" }, + { name = "agent-framework-azure-ai-search" }, + { name = "agent-framework-azurefunctions" }, + { name = "agent-framework-chatkit" }, + { name = "agent-framework-copilotstudio" }, + { name = "agent-framework-declarative" }, + { name = "agent-framework-devui" }, + { name = "agent-framework-lab" }, + { name = "agent-framework-mem0" }, + { name = "agent-framework-ollama" }, + { name = "agent-framework-purview" }, + { name = "agent-framework-redis" }, +] + +[[package]] +name = "agent-framework-declarative" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "powerfx", marker = "python_full_version < '3.14'" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/30/22fb13d4ae2a13a138ad245fcfbe9aa38f5b7dbdc0cd9672fd6db874ee92/agent_framework_declarative-1.0.0b260107.tar.gz", hash = "sha256:8edf62c8cae0c67e4cbdb713c0e35c4ceaf7ccabb6f1a2b950d4b8796e29bc84", size = 12757 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/0c/4db67ac51cfad217f1928e3f64ab512ca34e2a7b8d0dfe9e09c6fadecf80/agent_framework_declarative-1.0.0b260107-py3-none-any.whl", hash = "sha256:35004053cbfd0217cf802467d87f51324822be351dd67f5e12f9b851019bb5b0", size = 13510 }, +] + +[[package]] +name = "agent-framework-devui" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "fastapi" }, + { name = "python-dotenv" }, + { name = "uvicorn", extra = ["standard"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/cc/144aec868a2d599e5a779a3f17fb98c77ea4e9e8bd909c559981bc789252/agent_framework_devui-1.0.0b260107.tar.gz", hash = "sha256:af025563bd5e7ec626027610fb43553e33a741487465bc9abbcdf11f751860bb", size = 356007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/b9/c7b6c12b4e0bfd6f4d4671512cd5805477abf9d1d93201786d24d969bcf2/agent_framework_devui-1.0.0b260107-py3-none-any.whl", hash = "sha256:94039e7a0a0cddf343ee40fd3209bb16b9343c33fcbe288a1b31da19cd991260", size = 361044 }, +] + +[[package]] +name = "agent-framework-lab" +version = "1.0.0b251024" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/c5/be86273cb3545651d0c8112ff9f38ae8fe13b740ce9b65b9be83ff2d70ee/agent_framework_lab-1.0.0b251024.tar.gz", hash = "sha256:4261cb595b6edfd4f30db613c1885c71b3dcfa2088cf29224d4f17b3ff956b2a", size = 23397 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/0f/3974b2b1f6bf523ee3ced0886b6afd5ca8bbebd24aa5278ef77db0d3d765/agent_framework_lab-1.0.0b251024-py3-none-any.whl", hash = "sha256:1596408991a92fcacef4bb939305d2b59159517b707f48114105fc0dd46bfee7", size = 26589 }, +] + +[[package]] +name = "agent-framework-mem0" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "mem0ai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/49/8c000c562a0bfc2cdf160253a030fbc21771db69c009f9970902e1ddd65b/agent_framework_mem0-1.0.0b260107.tar.gz", hash = "sha256:11c9672e2cd7f2f74213472fd4abed26a913fa6443f9224804f3c9b1b58f74b7", size = 5400 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/07/fae73c5b0045dc78c685348371402a6dbadd83147da744a44ade7d7ad06d/agent_framework_mem0-1.0.0b260107-py3-none-any.whl", hash = "sha256:c52751565da07524bf2317fdd75068bdd03c73b7002d82acee393821485909e6", size = 5573 }, +] + +[[package]] +name = "agent-framework-ollama" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "ollama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ba/23eaba3ea5220f1752d8d4a398a41951c7f7b1fc650cf1fed48c7e4e5127/agent_framework_ollama-1.0.0b260107.tar.gz", hash = "sha256:412c098eedb170d76e15eadc5b0bc9f5792a7e13d655cb1e7f03e8e9fb4d6950", size = 5982 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/30/f821646487fb08018c240ca1ecbb5c4684378dfb48c192b6c1bf778dc286/agent_framework_ollama-1.0.0b260107-py3-none-any.whl", hash = "sha256:11c46a8495f58a71044c648476ff982fede1ad1e64cda28c9a9128ca3674d7b0", size = 7029 }, +] + +[[package]] +name = "agent-framework-purview" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "azure-core" }, + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/e7/097789fad41cdc4c477a78278a25e9af0e35c328dee612ad46bdbdda3e15/agent_framework_purview-1.0.0b260107.tar.gz", hash = "sha256:f12fb52b1d4ce0dc593458182ac901dafaf1bdcca9a86aa7cfe16f27546bcf89", size = 26814 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/43/04a107ae1d46a53c4f9423a87e75c352d4810665d6a8c0b2d28f06f92360/agent_framework_purview-1.0.0b260107-py3-none-any.whl", hash = "sha256:74d39279a84333a7e343fec2e2b4723700b58e2bdb3d18a315af3a03efd77018", size = 26176 }, +] + +[[package]] +name = "agent-framework-redis" +version = "1.0.0b260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "numpy" }, + { name = "redis" }, + { name = "redisvl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/9c/57332b52089240adba1fae311893bc003238434ddb31773e82d32a64b4b1/agent_framework_redis-1.0.0b260107.tar.gz", hash = "sha256:a66fb64646521967995ee0ea0970695c66d016838f3f8f965e0c21a406f48c41", size = 15714 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/6e/1aa99fc437481370f5256c23a29ff9899dd6e727af8b928fb06620b339a6/agent_framework_redis-1.0.0b260107-py3-none-any.whl", hash = "sha256:77a4276ece6c28ed65a53a1b399132fe2920f8da9bbd83eb87efb1eb41c44118", size = 16051 }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, +] + +[[package]] +name = "aiohttp" +version = "3.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732 }, + { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293 }, + { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533 }, + { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839 }, + { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932 }, + { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906 }, + { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020 }, + { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181 }, + { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794 }, + { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900 }, + { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239 }, + { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527 }, + { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489 }, + { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852 }, + { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379 }, + { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253 }, + { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407 }, + { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190 }, + { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783 }, + { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704 }, + { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652 }, + { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014 }, + { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777 }, + { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276 }, + { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131 }, + { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863 }, + { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793 }, + { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676 }, + { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217 }, + { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303 }, + { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673 }, + { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120 }, + { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383 }, + { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899 }, + { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238 }, + { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292 }, + { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021 }, + { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263 }, + { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107 }, + { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196 }, + { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591 }, + { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277 }, + { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575 }, + { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455 }, + { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417 }, + { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968 }, + { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690 }, + { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390 }, + { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188 }, + { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126 }, + { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128 }, + { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512 }, + { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444 }, + { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798 }, + { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835 }, + { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486 }, + { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951 }, + { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001 }, + { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246 }, + { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131 }, + { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196 }, + { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841 }, + { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193 }, + { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979 }, + { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193 }, + { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801 }, + { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523 }, + { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694 }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anthropic" +version = "0.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "docstring-parser" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/be/d11abafaa15d6304826438170f7574d750218f49a106c54424a40cef4494/anthropic-0.76.0.tar.gz", hash = "sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe", size = 495483 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/70/7b0fd9c1a738f59d3babe2b4212031c34ab7d0fda4ffef15b58a55c5bcea/anthropic-0.76.0-py3-none-any.whl", hash = "sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c", size = 390309 }, +] + +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592 }, +] + +[[package]] +name = "attrs" +version = "25.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, +] + +[[package]] +name = "azure-ai-agents" +version = "1.2.0b5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ed/57/8adeed578fa8984856c67b4229e93a58e3f6024417d448d0037aafa4ee9b/azure_ai_agents-1.2.0b5.tar.gz", hash = "sha256:1a16ef3f305898aac552269f01536c34a00473dedee0bca731a21fdb739ff9d5", size = 394876 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/6d/15070d23d7a94833a210da09d5d7ed3c24838bb84f0463895e5d159f1695/azure_ai_agents-1.2.0b5-py3-none-any.whl", hash = "sha256:257d0d24a6bf13eed4819cfa5c12fb222e5908deafb3cbfd5711d3a511cc4e88", size = 217948 }, +] + +[[package]] +name = "azure-ai-evaluation" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "azure-core" }, + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "msrest" }, + { name = "nltk" }, + { name = "openai" }, + { name = "pandas" }, + { name = "pyjwt" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/ab/62300008df848b210ef2a21b646480eee7c1bf3906afdc1351795343321c/azure_ai_evaluation-1.14.0.tar.gz", hash = "sha256:2a5681805b7cde65ad663f34d0f647d28498dd9395f7e2ce0789320c26664dae", size = 2196726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/68/1e0bd2123a5e681dbe474a3dda098c85704556e53ae24c7f4b3915d4e048/azure_ai_evaluation-1.14.0-py3-none-any.whl", hash = "sha256:1785f9be28517839ab9d30a03893951f7c9b530500d939d0ae51dde3aa1478b0", size = 1141136 }, +] + +[[package]] +name = "azure-ai-projects" +version = "2.0.0b3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "isodate" }, + { name = "openai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/24/e0/3512d3f07e9dd2eb4af684387c31598c435bd87833b6a81850972963cb9c/azure_ai_projects-2.0.0b3.tar.gz", hash = "sha256:6d09ad110086e450a47b991ee8a3644f1be97fa3085d5981d543f900d78f4505", size = 431749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/b6/8fbd4786bb5c0dd19eaff86ddce0fbfb53a6f90d712038272161067a076a/azure_ai_projects-2.0.0b3-py3-none-any.whl", hash = "sha256:3b3048a3ba3904d556ba392b7bd20b6e84c93bb39df6d43a6470cdb0ad08af8c", size = 240717 }, +] + +[[package]] +name = "azure-common" +version = "1.1.28" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462 }, +] + +[[package]] +name = "azure-core" +version = "1.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825 }, +] + +[[package]] +name = "azure-functions" +version = "1.25.0b3.dev1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "werkzeug" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/a3/8d6d1f3d7869363028a2488e6b3fed7375be0c652933a6b701dbe8ebff36/azure_functions-1.25.0b3.dev1.tar.gz", hash = "sha256:f9777661b0fd14e6a6ad7a85bb179ba59c80ffa64ec15f1728848154c9135c2e", size = 142121 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/3f/d3a446d76159cb1e2015e7a24b888d2affc28d68c59795252133e6474cad/azure_functions-1.25.0b3.dev1-py3-none-any.whl", hash = "sha256:3ba27c26310c112d0955e1dae19fa378b40b509ff1c59e1a45826a28042d21a3", size = 114184 }, +] + +[[package]] +name = "azure-functions-durable" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "azure-functions" }, + { name = "furl" }, + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "python-dateutil" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/3a/f168b434fa69eaaf5d14b54d88239b851eceb7e10f666b55289dd0933ccb/azure-functions-durable-1.4.0.tar.gz", hash = "sha256:945488ef28917dae4295a4dd6e6f6601ffabe32e3fbb94ceb261c9b65b6e6c0f", size = 176584 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/01/7f03229fa5c05a5cc7e41172aef80c5242d28aeea0825f592f93141a4b91/azure_functions_durable-1.4.0-py3-none-any.whl", hash = "sha256:0efe919cdda96924791feabe192a37c7d872414b4c6ce348417a02ee53d8cc31", size = 143159 }, +] + +[[package]] +name = "azure-identity" +version = "1.26.0b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "msal" }, + { name = "msal-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/b0/0c93d0d35694d5015f565a70ef5428ba640a3ba3bc082e24be4d72a3a915/azure_identity-1.26.0b1.tar.gz", hash = "sha256:401197087ec14ee29cfbfcd099453d56037bef252954fee04b5d26ccb702c869", size = 292298 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/28/af9ef022f21e3b51b3718d4348f771b490678c1116563895547c0a771362/azure_identity-1.26.0b1-py3-none-any.whl", hash = "sha256:dc608b59ae628a38611208ee761adeb1a2b9390258b58d6edcda2d24c50a4348", size = 197227 }, +] + +[[package]] +name = "azure-search-documents" +version = "11.7.0b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-common" }, + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/ba/bde0f03e0a742ba3bbcc929f91ed2f3b1420c2bb84c9a7f878f3b87ebfce/azure_search_documents-11.7.0b2.tar.gz", hash = "sha256:b6e039f8038ff2210d2057e704e867c6e29bb46bfcd400da4383e45e4b8bb189", size = 423956 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/26/ed4498374f9088818278ac225f2bea688b4ec979d81bf83a5355c8c366af/azure_search_documents-11.7.0b2-py3-none-any.whl", hash = "sha256:f82117b321344a84474269ed26df194c24cca619adc024d981b1b86aee3c6f05", size = 432037 }, +] + +[[package]] +name = "azure-storage-blob" +version = "12.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "cryptography" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/24/072ba8e27b0e2d8fec401e9969b429d4f5fc4c8d4f0f05f4661e11f7234a/azure_storage_blob-12.28.0.tar.gz", hash = "sha256:e7d98ea108258d29aa0efbfd591b2e2075fa1722a2fae8699f0b3c9de11eff41", size = 604225 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl", hash = "sha256:00fb1db28bf6a7b7ecaa48e3b1d5c83bfadacc5a678b77826081304bd87d6461", size = 431499 }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, +] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900 }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, +] + +[[package]] +name = "clr-loader" +version = "0.2.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004 }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667 }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807 }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615 }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800 }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707 }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541 }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464 }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838 }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596 }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782 }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381 }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988 }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451 }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007 }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012 }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728 }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078 }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460 }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237 }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344 }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564 }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415 }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457 }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074 }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569 }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941 }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339 }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315 }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331 }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248 }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089 }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029 }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222 }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280 }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958 }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714 }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970 }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236 }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642 }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126 }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573 }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695 }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720 }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740 }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, +] + +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 }, +] + +[[package]] +name = "fastapi" +version = "0.128.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094 }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782 }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594 }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448 }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411 }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014 }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909 }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049 }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485 }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619 }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320 }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820 }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518 }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096 }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985 }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591 }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102 }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717 }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651 }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417 }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391 }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048 }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549 }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833 }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363 }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314 }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365 }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763 }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110 }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717 }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235 }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742 }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725 }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506 }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161 }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676 }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638 }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067 }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101 }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901 }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395 }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659 }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127 }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698 }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749 }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298 }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015 }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038 }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130 }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845 }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131 }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542 }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308 }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210 }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972 }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536 }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330 }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627 }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238 }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738 }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739 }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186 }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196 }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830 }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289 }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318 }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814 }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762 }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470 }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042 }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148 }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676 }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451 }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507 }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 }, +] + +[[package]] +name = "furl" +version = "2.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "orderedmultidict" }, + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/e4/203a76fa2ef46cdb0a618295cc115220cbb874229d4d8721068335eb87f0/furl-2.1.4.tar.gz", hash = "sha256:877657501266c929269739fb5f5980534a41abd6bbabcb367c136d1d3b2a6015", size = 57526 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/8c/dce3b1b7593858eba995b2dfdb833f872c7f863e3da92aab7128a6b11af4/furl-2.1.4-py2.py3-none-any.whl", hash = "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8", size = 27550 }, +] + +[[package]] +name = "google-api-core" +version = "2.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906 }, +] + +[[package]] +name = "google-auth" +version = "2.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867 }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 }, +] + +[[package]] +name = "greenlet" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379 }, + { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294 }, + { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742 }, + { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297 }, + { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885 }, + { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424 }, + { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017 }, + { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964 }, + { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140 }, + { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219 }, + { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211 }, + { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311 }, + { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833 }, + { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256 }, + { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483 }, + { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833 }, + { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671 }, + { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360 }, + { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160 }, + { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388 }, + { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166 }, + { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193 }, + { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387 }, + { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638 }, + { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145 }, + { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236 }, + { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506 }, + { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783 }, + { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857 }, + { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034 }, +] + +[[package]] +name = "griffe" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705 }, +] + +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718 }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627 }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167 }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267 }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963 }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484 }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777 }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014 }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750 }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003 }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716 }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522 }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558 }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990 }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387 }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668 }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928 }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983 }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727 }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799 }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417 }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219 }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826 }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550 }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564 }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236 }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795 }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214 }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961 }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779 }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httptools" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280 }, + { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004 }, + { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655 }, + { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440 }, + { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186 }, + { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192 }, + { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694 }, + { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 }, + { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 }, + { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 }, + { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 }, + { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 }, + { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 }, + { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 }, + { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619 }, + { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714 }, + { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909 }, + { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831 }, + { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631 }, + { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910 }, + { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865 }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "isodate" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, +] + +[[package]] +name = "jiter" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449 }, + { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855 }, + { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171 }, + { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590 }, + { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462 }, + { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983 }, + { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328 }, + { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740 }, + { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875 }, + { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457 }, + { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546 }, + { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196 }, + { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100 }, + { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 }, + { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 }, + { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 }, + { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 }, + { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 }, + { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 }, + { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 }, + { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 }, + { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 }, + { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 }, + { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 }, + { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 }, + { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 }, + { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 }, + { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 }, + { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 }, + { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 }, + { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 }, + { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 }, + { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 }, + { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 }, + { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 }, + { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 }, + { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 }, + { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 }, + { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 }, + { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 }, + { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 }, + { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 }, + { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 }, + { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 }, + { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 }, + { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 }, + { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 }, + { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 }, + { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 }, + { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 }, + { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 }, + { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 }, + { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 }, + { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 }, + { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 }, + { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974 }, + { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233 }, + { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537 }, + { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071 }, +] + +[[package]] +name = "jsonpath-ng" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ply" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/86/08646239a313f895186ff0a4573452038eed8c86f54380b3ebac34d32fb2/jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c", size = 37838 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/5a/73ecb3d82f8615f32ccdadeb9356726d6cae3a4bbc840b437ceb95708063/jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6", size = 30105 }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, +] + +[[package]] +name = "mcp" +version = "1.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076 }, +] + +[package.optional-dependencies] +ws = [ + { name = "websockets" }, +] + +[[package]] +name = "mem0ai" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "openai" }, + { name = "posthog" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "pytz" }, + { name = "qdrant-client" }, + { name = "sqlalchemy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/b3/57edb1253e7dc24d41e102722a585d6e08a96c6191a6a04e43112c01dc5d/mem0ai-1.0.2.tar.gz", hash = "sha256:533c370e8a4e817d47a583cb7fa4df55db59de8dd67be39f2b927e2ad19607d1", size = 182395 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/82/59309070bd2d2ddccebd89d8ebb7a2155ce12531f0c36123d0a39eada544/mem0ai-1.0.2-py3-none-any.whl", hash = "sha256:3528523653bc57efa477d55e703dcedf8decc23868d4dbcc6d43a97f2315834a", size = 275428 }, +] + +[[package]] +name = "microsoft-agents-activity" +version = "0.7.0.dev12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/02/9c9eb63917392883ad371f1f8c534adfb68deeb0a2ffcf489951a3a5ebc6/microsoft_agents_activity-0.7.0.dev12.tar.gz", hash = "sha256:0b3d7ca7af9559729e32aa2c64aef6de4426a0d8357af7a55f5a8cded5d084a9", size = 60983 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/71/e946dfe26df5c57487c587b95e05c77f39a6a75181caad0a2e47fe2b0b70/microsoft_agents_activity-0.7.0.dev12-py3-none-any.whl", hash = "sha256:fb87ce08abe35e7e1226db34a76a2a6303989fa4f6ee3f82b39c51440d999cd8", size = 132661 }, +] + +[[package]] +name = "microsoft-agents-copilotstudio-client" +version = "0.7.0.dev12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "microsoft-agents-hosting-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/88/e2/ee2077a873377c3d6832fb44c339dd2b1db9b65e53e9cdd1b460daa8aef2/microsoft_agents_copilotstudio_client-0.7.0.dev12.tar.gz", hash = "sha256:cab5c1bc149bbd3b32ce3f00ecdb38ff00664f180d93f882a5e65fa738d6ff88", size = 12648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/67/4d01168a35c4dd7ed97f9588143e3eea8f4894ce5de8401428da0dad3fc9/microsoft_agents_copilotstudio_client-0.7.0.dev12-py3-none-any.whl", hash = "sha256:c78682deb416652957992436b47c864c4287da377fe48fcd2bfef3eacf99cc75", size = 13494 }, +] + +[[package]] +name = "microsoft-agents-hosting-core" +version = "0.7.0.dev12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "microsoft-agents-activity" }, + { name = "pyjwt" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/ca/ffe2f0ed6aa9f7a2a9d793539003fee0e86622b83a61f0933065de7b7953/microsoft_agents_hosting_core-0.7.0.dev12.tar.gz", hash = "sha256:8093ced5a435cb2fb177be38dd1eeaec937aefa544ec1371f65b41dd53a3721d", size = 90609 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/19/b09facedd92b439ffed675ceaf611bd9107acfcf77df531816587019f865/microsoft_agents_hosting_core-0.7.0.dev12-py3-none-any.whl", hash = "sha256:cca0d752c8ce055cc53211e0e3e501466ac629bf50f391550c9f029b791b620e", size = 133796 }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927 }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464 }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002 }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222 }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793 }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888 }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993 }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956 }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224 }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798 }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083 }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111 }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453 }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612 }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145 }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781 }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145 }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230 }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032 }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353 }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085 }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358 }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332 }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612 }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825 }, +] + +[[package]] +name = "msal" +version = "1.35.0b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/7a/6880016fab1720981b54db844c32af6f2e5e90aac21575ad6e54e1840313/msal-1.35.0b1.tar.gz", hash = "sha256:fe8143079183a5c952cd9f3ba66a148fe7bae9fb9952bd0e834272bfbeb34508", size = 157573 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/8e/7090fafcf58e9081767a8fa960431c708211ce273bc4f6e519e9046acacc/msal-1.35.0b1-py3-none-any.whl", hash = "sha256:bf656775c64bbc2103d8255980f5c3c966c7432106795e1fe70ca338a7e43150", size = 117733 }, +] + +[[package]] +name = "msal-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "msal" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, +] + +[[package]] +name = "msrest" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "certifi" }, + { name = "isodate" }, + { name = "requests" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384 }, +] + +[[package]] +name = "multidict" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877 }, + { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467 }, + { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834 }, + { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545 }, + { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305 }, + { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363 }, + { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375 }, + { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346 }, + { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107 }, + { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592 }, + { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024 }, + { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484 }, + { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579 }, + { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654 }, + { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511 }, + { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895 }, + { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073 }, + { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226 }, + { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135 }, + { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117 }, + { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472 }, + { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342 }, + { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082 }, + { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704 }, + { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355 }, + { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259 }, + { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903 }, + { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365 }, + { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062 }, + { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683 }, + { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254 }, + { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967 }, + { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085 }, + { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713 }, + { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915 }, + { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077 }, + { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114 }, + { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442 }, + { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885 }, + { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588 }, + { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966 }, + { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618 }, + { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539 }, + { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345 }, + { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934 }, + { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243 }, + { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878 }, + { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452 }, + { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312 }, + { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935 }, + { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385 }, + { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777 }, + { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104 }, + { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503 }, + { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128 }, + { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410 }, + { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205 }, + { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084 }, + { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667 }, + { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590 }, + { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112 }, + { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194 }, + { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510 }, + { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395 }, + { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520 }, + { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479 }, + { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903 }, + { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333 }, + { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411 }, + { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940 }, + { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087 }, + { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368 }, + { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326 }, + { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065 }, + { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475 }, + { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324 }, + { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877 }, + { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824 }, + { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558 }, + { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339 }, + { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895 }, + { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862 }, + { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376 }, + { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272 }, + { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774 }, + { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731 }, + { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193 }, + { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023 }, + { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507 }, + { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804 }, + { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317 }, +] + +[[package]] +name = "nltk" +version = "3.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404 }, +] + +[[package]] +name = "numpy" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888 }, + { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956 }, + { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567 }, + { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459 }, + { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859 }, + { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419 }, + { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131 }, + { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342 }, + { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015 }, + { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730 }, + { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166 }, + { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495 }, + { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657 }, + { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256 }, + { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212 }, + { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871 }, + { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305 }, + { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909 }, + { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380 }, + { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089 }, + { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230 }, + { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125 }, + { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156 }, + { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663 }, + { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224 }, + { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352 }, + { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279 }, + { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316 }, + { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884 }, + { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138 }, + { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478 }, + { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981 }, + { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046 }, + { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858 }, + { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417 }, + { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643 }, + { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963 }, + { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811 }, + { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643 }, + { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601 }, + { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722 }, + { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590 }, + { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180 }, + { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774 }, + { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274 }, + { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306 }, + { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653 }, + { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144 }, + { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425 }, + { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053 }, + { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482 }, + { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117 }, + { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121 }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 }, +] + +[[package]] +name = "ollama" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354 }, +] + +[[package]] +name = "openai" +version = "2.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879 }, +] + +[[package]] +name = "openai-agents" +version = "0.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffe" }, + { name = "mcp" }, + { name = "openai" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "types-requests" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e7/5c/5ebface62a0efdc7298152dcd2d32164403e25e53f1088c042936d8d40f9/openai_agents-0.6.5.tar.gz", hash = "sha256:67e8cab27082d1a1fe6f3fecfcf89b41ff249988a75640bbcc2764952d603ef0", size = 2044506 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/db/16020e45d53366f2ed653ce0ddf959a647687d47180954de7654a133b910/openai_agents-0.6.5-py3-none-any.whl", hash = "sha256:c81d2eaa5c4563b8e893ba836fe170cf10ba974420ff283b4f001f84e7cb6e6b", size = 249352 }, +] + +[[package]] +name = "openai-chatkit" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "openai" }, + { name = "openai-agents" }, + { name = "pydantic" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/f3/3e7aafd6c29348e60d32082fb14e539661fe4100453a31b34d0fef1ff7b7/openai_chatkit-1.5.2.tar.gz", hash = "sha256:187d27b815f153fa060337c86ee3aab189f72269f23ac2bb2a35c6c88b83846d", size = 59268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/b6/475a4c723fb2e0de30feea505505eabe77666aa7d81855d356fb289e3d8a/openai_chatkit-1.5.2-py3-none-any.whl", hash = "sha256:3bf3f140f314924ef1d4148ce5174cff6aa4c5d1760f988ba2aa267fd434f960", size = 41482 }, +] + +[[package]] +name = "opentelemetry-api" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356 }, +] + +[[package]] +name = "opentelemetry-sdk" +version = "1.39.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.60b1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982 }, +] + +[[package]] +name = "opentelemetry-semantic-conventions-ai" +version = "0.4.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e6/40b59eda51ac47009fb47afcdf37c6938594a0bd7f3b9fadcbc6058248e3/opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036", size = 5368 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080 }, +] + +[[package]] +name = "orderedmultidict" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5c/62/61ad51f6c19d495970230a7747147ce7ed3c3a63c2af4ebfdb1f6d738703/orderedmultidict-1.0.2.tar.gz", hash = "sha256:16a7ae8432e02cc987d2d6d5af2df5938258f87c870675c73ee77a0920e6f4a6", size = 13973 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/6c/d8a02ffb24876b5f51fbd781f479fc6525a518553a4196bd0433dae9ff8e/orderedmultidict-1.0.2-py2.py3-none-any.whl", hash = "sha256:ab5044c1dca4226ae4c28524cfc5cc4c939f0b49e978efa46a6ad6468049f79b", size = 11897 }, +] + +[[package]] +name = "packaging" +version = "26.0rc2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/29/b1656a8724cb5d53eb011bdb8747ade15e6a875d23a1b99bba09cd8db264/packaging-26.0rc2.tar.gz", hash = "sha256:51c9779f69ab1f6ed1a4d6d0e2f42e2e64b566955a5eff1f7f83bcab688035a4", size = 142648 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/eb/1f8f5e3b10748612b075b2b991d6c4342d993008d2aa05f5c872a4e7bfa5/packaging-26.0rc2-py3-none-any.whl", hash = "sha256:885e01b9dbe4913e5080fa516b8550d43ef38549088c63e6e8bb51cd25adea4a", size = 74124 }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693 }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002 }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971 }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722 }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "ply" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567 }, +] + +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424 }, +] + +[[package]] +name = "posthog" +version = "7.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/3b/866af11cb12e9d35feffcd480d4ebf31f87b2164926b9c670cbdafabc814/posthog-7.5.1.tar.gz", hash = "sha256:d8a8165b3d47465023ea2f919982a34890e2dda76402ec47d6c68424b2534a55", size = 145244 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/03/ba011712ce9d07fe87dcfb72474c388d960e6d0c4f2262d2ae11fd27f0c5/posthog-7.5.1-py3-none-any.whl", hash = "sha256:fd3431ce32c9bbfb1e3775e3633c32ee589c052b0054fafe5ed9e4b17c1969d3", size = 167555 }, +] + +[[package]] +name = "powerfx" +version = "0.0.34" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "python_full_version < '3.14'" }, + { name = "pythonnet", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/96/0f8a1f86485b3ec0315e3e8403326884a0334b3dcd699df2482669cca4be/powerfx-0.0.34-py3-none-any.whl", hash = "sha256:f2dc1c42ba8bfa4c72a7fcff2a00755b95394547388ca0b3e36579c49ee7ed75", size = 3483089 }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061 }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037 }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324 }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505 }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242 }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474 }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575 }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736 }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019 }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376 }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988 }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615 }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066 }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655 }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789 }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 }, +] + +[[package]] +name = "proto-plus" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/01/89/9cbe2f4bba860e149108b683bc2efec21f14d5f7ed6e25562ad86acbc373/proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4", size = 56158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", size = 50205 }, +] + +[[package]] +name = "protobuf" +version = "5.29.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963 }, + { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818 }, + { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091 }, + { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824 }, + { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942 }, + { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823 }, +] + +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, +] + +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, +] + +[[package]] +name = "pydantic-settings" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382 }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, +] + +[[package]] +name = "python-multipart" +version = "0.0.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541 }, +] + +[[package]] +name = "python-ulid" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577 }, +] + +[[package]] +name = "pythonnet" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "clr-loader", marker = "python_full_version < '3.14'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506 }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, +] + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +] + +[[package]] +name = "qdrant-client" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "grpcio" }, + { name = "httpx", extra = ["http2"] }, + { name = "numpy" }, + { name = "portalocker" }, + { name = "protobuf" }, + { name = "pydantic" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ca/7d/3cd10e26ae97b35cf856ca1dc67576e42414ae39502c51165bb36bb1dff8/qdrant_client-1.16.2.tar.gz", hash = "sha256:ca4ef5f9be7b5eadeec89a085d96d5c723585a391eb8b2be8192919ab63185f0", size = 331112 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/13/8ce16f808297e16968269de44a14f4fef19b64d9766be1d6ba5ba78b579d/qdrant_client-1.16.2-py3-none-any.whl", hash = "sha256:442c7ef32ae0f005e88b5d3c0783c63d4912b97ae756eb5e052523be682f17d3", size = 377186 }, +] + +[[package]] +name = "redis" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159 }, +] + +[[package]] +name = "redisvl" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonpath-ng" }, + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "python-ulid" }, + { name = "pyyaml" }, + { name = "redis" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/d6/8f3235b272e3a2370698d7524aad2dec15f53c5be5d6726ba41056844f69/redisvl-0.13.2.tar.gz", hash = "sha256:f34c4350922ac469c45d90b5db65c49950e6aa8706331931b000f631ff9a0f4a", size = 737736 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/93/81ea5c45637ce7fe2fdaf214d5e1b91afe96a472edeb9b659e24d3710dfb/redisvl-0.13.2-py3-none-any.whl", hash = "sha256:dd998c6acc54f13526d464ad6b6e6f0c4cf6985fb2c7a1655bdf8ed8e57a4c01", size = 192760 }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, +] + +[[package]] +name = "regex" +version = "2025.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312 }, + { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256 }, + { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921 }, + { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568 }, + { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165 }, + { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182 }, + { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501 }, + { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842 }, + { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519 }, + { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611 }, + { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759 }, + { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194 }, + { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069 }, + { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330 }, + { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081 }, + { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123 }, + { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814 }, + { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592 }, + { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122 }, + { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272 }, + { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497 }, + { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892 }, + { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462 }, + { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528 }, + { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866 }, + { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189 }, + { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054 }, + { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325 }, + { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984 }, + { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673 }, + { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029 }, + { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437 }, + { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368 }, + { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921 }, + { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708 }, + { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472 }, + { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341 }, + { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666 }, + { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473 }, + { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792 }, + { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214 }, + { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469 }, + { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089 }, + { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059 }, + { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900 }, + { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010 }, + { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893 }, + { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522 }, + { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272 }, + { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958 }, + { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289 }, + { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026 }, + { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499 }, + { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604 }, + { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320 }, + { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372 }, + { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985 }, + { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669 }, + { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030 }, + { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674 }, + { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451 }, + { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980 }, + { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852 }, + { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566 }, + { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463 }, + { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694 }, + { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691 }, + { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583 }, + { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286 }, + { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, +] + +[[package]] +name = "rpds-py" +version = "0.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086 }, + { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053 }, + { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763 }, + { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951 }, + { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622 }, + { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492 }, + { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080 }, + { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680 }, + { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589 }, + { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289 }, + { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737 }, + { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120 }, + { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782 }, + { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463 }, + { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868 }, + { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887 }, + { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904 }, + { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945 }, + { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783 }, + { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021 }, + { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589 }, + { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025 }, + { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895 }, + { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799 }, + { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731 }, + { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027 }, + { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020 }, + { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 }, + { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 }, + { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 }, + { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443 }, + { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375 }, + { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850 }, + { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812 }, + { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841 }, + { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149 }, + { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843 }, + { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507 }, + { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949 }, + { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790 }, + { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217 }, + { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806 }, + { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 }, + { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 }, + { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099 }, + { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192 }, + { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080 }, + { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841 }, + { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670 }, + { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005 }, + { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112 }, + { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049 }, + { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661 }, + { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606 }, + { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126 }, + { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371 }, + { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 }, + { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 }, + { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 }, + { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868 }, + { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747 }, + { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795 }, + { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330 }, + { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194 }, + { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340 }, + { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765 }, + { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834 }, + { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470 }, + { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630 }, + { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148 }, + { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030 }, + { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, + { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, +] + +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, +] + +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760 }, + { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268 }, + { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144 }, + { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907 }, + { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182 }, + { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200 }, + { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082 }, + { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131 }, + { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389 }, + { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054 }, + { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299 }, + { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264 }, + { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998 }, + { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434 }, + { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404 }, + { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057 }, + { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279 }, + { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508 }, + { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204 }, + { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785 }, + { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029 }, + { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142 }, + { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672 }, +] + +[[package]] +name = "sse-starlette" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484 }, +] + +[[package]] +name = "starlette" +version = "0.50.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, +] + +[[package]] +name = "tenacity" +version = "9.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + +[[package]] +name = "tzdata" +version = "2025.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, +] + +[[package]] +name = "uvicorn" +version = "0.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502 }, +] + +[package.optional-dependencies] +standard = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936 }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769 }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413 }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307 }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970 }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343 }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745 }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769 }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374 }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485 }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813 }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816 }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186 }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812 }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196 }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657 }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042 }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410 }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209 }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 }, +] + +[[package]] +name = "websockets" +version = "16.0" +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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261 }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693 }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255 }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689 }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915 }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381 }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486 }, + { 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 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062 }, + { 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 }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085 }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531 }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598 }, +] + +[[package]] +name = "werkzeug" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025 }, +] + +[[package]] +name = "yarl" +version = "1.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000 }, + { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338 }, + { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909 }, + { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940 }, + { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825 }, + { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705 }, + { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518 }, + { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267 }, + { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797 }, + { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535 }, + { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324 }, + { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803 }, + { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220 }, + { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589 }, + { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213 }, + { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330 }, + { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980 }, + { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424 }, + { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821 }, + { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243 }, + { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361 }, + { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036 }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671 }, + { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059 }, + { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356 }, + { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331 }, + { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590 }, + { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316 }, + { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431 }, + { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555 }, + { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965 }, + { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205 }, + { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209 }, + { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966 }, + { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312 }, + { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967 }, + { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949 }, + { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818 }, + { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626 }, + { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129 }, + { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776 }, + { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879 }, + { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996 }, + { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047 }, + { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947 }, + { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943 }, + { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715 }, + { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857 }, + { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520 }, + { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504 }, + { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282 }, + { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080 }, + { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696 }, + { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121 }, + { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080 }, + { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661 }, + { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645 }, + { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361 }, + { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451 }, + { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814 }, + { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799 }, + { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990 }, + { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292 }, + { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888 }, + { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223 }, + { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981 }, + { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303 }, + { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820 }, + { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203 }, + { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173 }, + { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562 }, + { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828 }, + { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551 }, + { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512 }, + { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400 }, + { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140 }, + { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473 }, + { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056 }, + { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292 }, + { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171 }, + { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814 }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, +] diff --git a/tests/pytest.ini b/tests/pytest.ini index 3c21701d9..3d63c2667 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -2,9 +2,14 @@ markers = integration: marks tests as integration tests (require deployed services) unit: marks tests as unit tests (run locally without external services) + evaluation: marks tests as agent evaluation tests + slow: marks tests as slow-running (may incur API costs) # Default options addopts = -v --tb=short # Timeout for individual tests (in seconds) timeout = 300 + +# Async mode for pytest-asyncio +asyncio_mode = auto diff --git a/tests/requirements.txt b/tests/requirements.txt index aa6aa5bc9..09012f1a3 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -5,4 +5,8 @@ pytest-timeout requests azure-identity azure-keyvault-secrets -fastmcp \ No newline at end of file +fastmcp + +# Agent Evaluation dependencies +azure-ai-evaluation +python-dotenv \ No newline at end of file diff --git a/tests/test_agent_evaluation.py b/tests/test_agent_evaluation.py new file mode 100644 index 000000000..2ec52d0c0 --- /dev/null +++ b/tests/test_agent_evaluation.py @@ -0,0 +1,512 @@ +""" +Agent Evaluation Tests +====================== +Pytest-based evaluation tests for the single agent. + +These tests can be run: +1. Locally with MCP server running: `pytest tests/test_agent_evaluation.py -v` +2. In CI/CD pipeline against deployed services + +Markers: +- @pytest.mark.evaluation: All evaluation tests +- @pytest.mark.slow: Tests that take longer (full evaluation) +- @pytest.mark.unit: Fast unit tests for evaluation utilities +""" + +import asyncio +import json +import os +import sys +from pathlib import Path +from typing import Any, Dict, List +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +# Add paths for imports - use absolute paths for reliability +_tests_dir = Path(__file__).parent.resolve() +_repo_root = _tests_dir.parent.resolve() +sys.path.insert(0, str(_tests_dir / "evaluation")) +sys.path.insert(0, str(_tests_dir)) +sys.path.insert(0, str(_repo_root / "agentic_ai" / "applications")) +sys.path.insert(0, str(_repo_root / "agentic_ai")) + +from evaluation.agent_evaluator import ( + AgentEvaluator, + AgentResponse, + AgentRunner, + EvaluationThresholds, + TestCase, + ToolCallTracker, + load_test_data, +) + + +# ============================================================================ +# Fixtures +# ============================================================================ + +@pytest.fixture +def sample_test_case() -> TestCase: + """Create a sample test case for testing.""" + return TestCase( + query="What's my billing summary?", + customer_id="251", + expected_intent="billing_inquiry", + expected_tools=["get_billing_summary", "get_customer_detail"], + ground_truth="The agent should retrieve and present the customer's billing summary.", + category="billing", + complexity="low", + ) + + +@pytest.fixture +def sample_agent_response(sample_test_case: TestCase) -> AgentResponse: + """Create a sample agent response for testing.""" + return AgentResponse( + test_case=sample_test_case, + response="Based on your account, your current billing summary shows an outstanding balance of $150.00. This includes your monthly subscription of $99.99 and additional data usage charges of $50.01.", + tools_called=["get_customer_detail", "get_billing_summary"], + execution_time_ms=1500.0, + error=None, + ) + + +@pytest.fixture +def test_data_path() -> str: + """Get the path to the test data file.""" + return str(Path(__file__).parent / "evaluation" / "test_data.jsonl") + + +@pytest.fixture +def evaluator() -> AgentEvaluator: + """Create an evaluator instance with default thresholds.""" + return AgentEvaluator(thresholds=EvaluationThresholds()) + + +# ============================================================================ +# Unit Tests - Evaluation Utilities +# ============================================================================ + +@pytest.mark.unit +class TestTestCase: + """Tests for TestCase dataclass.""" + + def test_from_dict(self): + """Test creating TestCase from dictionary.""" + data = { + "query": "Test query", + "customer_id": "123", + "expected_intent": "test_intent", + "expected_tools": ["tool1", "tool2"], + "ground_truth": "Expected response", + "category": "test", + "complexity": "low", + } + + test_case = TestCase.from_dict(data) + + assert test_case.query == "Test query" + assert test_case.customer_id == "123" + assert test_case.expected_intent == "test_intent" + assert test_case.expected_tools == ["tool1", "tool2"] + assert test_case.ground_truth == "Expected response" + assert test_case.category == "test" + assert test_case.complexity == "low" + + +@pytest.mark.unit +class TestToolCallTracker: + """Tests for ToolCallTracker.""" + + @pytest.mark.asyncio + async def test_tracks_tool_calls(self): + """Test that tool calls are tracked correctly.""" + tracker = ToolCallTracker() + + await tracker.broadcast("session1", {"type": "tool_called", "tool_name": "get_customer_detail"}) + await tracker.broadcast("session1", {"type": "tool_called", "tool_name": "get_billing_summary"}) + + tools = tracker.get_tools_called() + assert "get_customer_detail" in tools + assert "get_billing_summary" in tools + assert len(tools) == 2 + + @pytest.mark.asyncio + async def test_ignores_non_tool_events(self): + """Test that non-tool events are ignored.""" + tracker = ToolCallTracker() + + await tracker.broadcast("session1", {"type": "agent_start"}) + await tracker.broadcast("session1", {"type": "agent_token", "content": "Hello"}) + + tools = tracker.get_tools_called() + assert len(tools) == 0 + + @pytest.mark.asyncio + async def test_deduplicates_tool_calls(self): + """Test that duplicate tool calls are not counted twice.""" + tracker = ToolCallTracker() + + await tracker.broadcast("session1", {"type": "tool_called", "tool_name": "get_customer_detail"}) + await tracker.broadcast("session1", {"type": "tool_called", "tool_name": "get_customer_detail"}) + + tools = tracker.get_tools_called() + assert len(tools) == 1 + + +@pytest.mark.unit +class TestLoadTestData: + """Tests for test data loading.""" + + def test_load_test_data(self, test_data_path: str): + """Test loading test data from JSONL file.""" + test_cases = load_test_data(test_data_path) + + assert len(test_cases) > 0 + assert all(isinstance(tc, TestCase) for tc in test_cases) + + # Check first test case has expected fields + first_case = test_cases[0] + assert first_case.query + assert first_case.customer_id + assert first_case.expected_intent + assert len(first_case.expected_tools) > 0 + + def test_load_test_data_categories(self, test_data_path: str): + """Test that test data covers multiple categories.""" + test_cases = load_test_data(test_data_path) + + categories = set(tc.category for tc in test_cases) + + # Should have at least billing and support categories + assert "billing" in categories + assert len(categories) >= 3 # At least 3 different categories + + +# ============================================================================ +# Unit Tests - Evaluator +# ============================================================================ + +@pytest.mark.unit +class TestAgentEvaluator: + """Tests for AgentEvaluator.""" + + def test_evaluate_tool_accuracy_perfect_match(self, evaluator: AgentEvaluator, sample_agent_response: AgentResponse): + """Test tool accuracy with perfect match.""" + result = evaluator.evaluate_tool_accuracy(sample_agent_response) + + assert result["tool_precision"] == 1.0 + assert result["tool_recall"] == 1.0 + assert result["tool_f1_score"] == 1.0 + assert result["passed"] is True + assert len(result["missing_tools"]) == 0 + assert len(result["extra_tools"]) == 0 + + def test_evaluate_tool_accuracy_missing_tools(self, evaluator: AgentEvaluator, sample_test_case: TestCase): + """Test tool accuracy when expected tools are missing.""" + response = AgentResponse( + test_case=sample_test_case, + response="Some response", + tools_called=["get_customer_detail"], # Missing get_billing_summary + execution_time_ms=1000.0, + ) + + result = evaluator.evaluate_tool_accuracy(response) + + assert result["tool_recall"] == 0.5 + assert result["tool_precision"] == 1.0 + assert result["missing_tools"] == ["get_billing_summary"] + + def test_evaluate_tool_accuracy_extra_tools(self, evaluator: AgentEvaluator, sample_test_case: TestCase): + """Test tool accuracy when extra tools are called.""" + response = AgentResponse( + test_case=sample_test_case, + response="Some response", + tools_called=["get_customer_detail", "get_billing_summary", "get_promotions"], + execution_time_ms=1000.0, + ) + + result = evaluator.evaluate_tool_accuracy(response) + + assert result["tool_recall"] == 1.0 + assert result["tool_precision"] < 1.0 + assert "get_promotions" in result["extra_tools"] + + def test_evaluate_tool_accuracy_no_tools_called(self, evaluator: AgentEvaluator, sample_test_case: TestCase): + """Test tool accuracy when no tools are called.""" + response = AgentResponse( + test_case=sample_test_case, + response="I cannot help with that.", + tools_called=[], + execution_time_ms=500.0, + ) + + result = evaluator.evaluate_tool_accuracy(response) + + assert result["tool_recall"] == 0.0 + assert result["passed"] is False + + def test_evaluate_response_quality_good_response(self, evaluator: AgentEvaluator, sample_agent_response: AgentResponse): + """Test response quality with a good response.""" + result = evaluator.evaluate_response_quality(sample_agent_response) + + assert result["has_content"] is True + assert result["word_count"] > 10 + assert result["passed"] is True + + def test_evaluate_response_quality_empty_response(self, evaluator: AgentEvaluator, sample_test_case: TestCase): + """Test response quality with empty response.""" + response = AgentResponse( + test_case=sample_test_case, + response="", + tools_called=[], + execution_time_ms=500.0, + ) + + result = evaluator.evaluate_response_quality(response) + + assert result["has_content"] is False + assert result["passed"] is False + + def test_evaluate_response_quality_with_error(self, evaluator: AgentEvaluator, sample_test_case: TestCase): + """Test response quality when there's an error.""" + response = AgentResponse( + test_case=sample_test_case, + response="", + tools_called=[], + execution_time_ms=100.0, + error="Connection timeout", + ) + + result = evaluator.evaluate_response_quality(response) + + assert result["has_error"] is True + assert result["passed"] is False + + +# ============================================================================ +# Integration Tests - Full Evaluation Pipeline +# ============================================================================ + +@pytest.mark.evaluation +@pytest.mark.integration +class TestAgentEvaluationIntegration: + """ + Integration tests that run the actual agent against test cases. + + These tests require: + - MCP server running + - Azure OpenAI credentials configured + """ + + @pytest.fixture + def check_environment(self): + """Check that required environment variables are set.""" + required_vars = [ + "AZURE_OPENAI_ENDPOINT", + "AZURE_OPENAI_CHAT_DEPLOYMENT", + "MCP_SERVER_URI", + ] + + missing = [var for var in required_vars if not os.getenv(var)] + + if missing: + pytest.skip(f"Missing required environment variables: {', '.join(missing)}") + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_single_agent_billing_query(self, check_environment, test_data_path: str): + """Test single agent with a billing query.""" + test_cases = load_test_data(test_data_path) + + # Find a billing test case + billing_case = next((tc for tc in test_cases if tc.category == "billing"), None) + if not billing_case: + pytest.skip("No billing test case found") + + runner = AgentRunner(agent_module="agents.agent_framework.single_agent") + response = await runner.run_single_test(billing_case) + + # Basic assertions + assert response.response, "Agent should return a response" + assert response.error is None, f"Agent should not error: {response.error}" + + # Evaluate the response + evaluator = AgentEvaluator() + result = await evaluator.evaluate_response(response, include_ai_eval=False) + + print(f"\nTest Case: {billing_case.query}") + print(f"Response: {response.response[:200]}...") + print(f"Tools Called: {response.tools_called}") + print(f"Tool F1 Score: {result['tool_accuracy']['tool_f1_score']:.2f}") + print(f"Passed: {result['passed']}") + + @pytest.mark.asyncio + @pytest.mark.slow + async def test_full_evaluation_pipeline(self, check_environment, test_data_path: str): + """ + Run the full evaluation pipeline on all test cases. + + This is a comprehensive test that runs all test cases and generates + evaluation metrics. Use with caution as it can be slow and costly. + """ + test_cases = load_test_data(test_data_path) + + # Limit to first 3 cases for CI to save time/cost + test_cases = test_cases[:3] + + runner = AgentRunner(agent_module="agents.agent_framework.single_agent") + responses = await runner.run_test_dataset(test_cases) + + evaluator = AgentEvaluator() + results = await evaluator.evaluate_all(responses, include_ai_eval=False) + + summary = results["summary"] + + print(f"\n{'='*60}") + print("EVALUATION RESULTS") + print(f"{'='*60}") + print(f"Total: {summary['total_tests']}") + print(f"Passed: {summary['passed']}") + print(f"Pass Rate: {summary['pass_rate']:.1%}") + print(f"Avg Tool F1: {summary['average_tool_f1_score']:.2f}") + + # Assertions for CI/CD gates + # Note: Tool F1 may be lower because agent uses subset of expected tools + # The key is that agent provides helpful responses + assert summary["average_tool_f1_score"] >= 0.3, "Average tool F1 should be at least 0.3" + + # Print individual results for debugging + for i, result in enumerate(results["individual_results"]): + print(f"\nTest {i+1}: {result['test_case']['query'][:50]}...") + print(f" Tools Called: {result['tool_accuracy']['called_tools']}") + print(f" Tool F1: {result['tool_accuracy']['tool_f1_score']:.2f}") + print(f" Response OK: {result['response_quality']['has_content']}") + + +# ============================================================================ +# Mocked Tests - For CI/CD without live services +# ============================================================================ + +@pytest.mark.evaluation +@pytest.mark.unit +class TestAgentEvaluationMocked: + """ + Mocked evaluation tests that don't require live services. + These tests verify the evaluation logic works correctly. + """ + + @pytest.mark.asyncio + async def test_evaluation_with_mocked_agent(self, test_data_path: str): + """Test evaluation pipeline with mocked agent responses.""" + test_cases = load_test_data(test_data_path)[:2] + + # Create mock responses + mock_responses = [ + AgentResponse( + test_case=tc, + response=f"Here is the information for customer {tc.customer_id}: " + + "Your account shows normal activity. " * 10, + tools_called=tc.expected_tools[:2], # Simulate calling some expected tools + execution_time_ms=1500.0, + ) + for tc in test_cases + ] + + evaluator = AgentEvaluator() + results = await evaluator.evaluate_all(mock_responses, include_ai_eval=False) + + assert results["summary"]["total_tests"] == 2 + assert "individual_results" in results + assert len(results["individual_results"]) == 2 + + @pytest.mark.asyncio + async def test_evaluation_handles_errors_gracefully(self): + """Test that evaluation handles agent errors gracefully.""" + test_case = TestCase( + query="Test query", + customer_id="999", + expected_intent="test", + expected_tools=["some_tool"], + ground_truth="Expected response", + category="test", + complexity="low", + ) + + error_response = AgentResponse( + test_case=test_case, + response="", + tools_called=[], + execution_time_ms=100.0, + error="Agent initialization failed", + ) + + evaluator = AgentEvaluator() + result = await evaluator.evaluate_response(error_response, include_ai_eval=False) + + assert result["passed"] is False + assert result["error"] == "Agent initialization failed" + assert result["response_quality"]["has_error"] is True + + +# ============================================================================ +# Threshold Tests +# ============================================================================ + +@pytest.mark.evaluation +@pytest.mark.unit +class TestEvaluationThresholds: + """Tests for evaluation threshold configuration.""" + + def test_default_thresholds(self): + """Test that default thresholds are reasonable.""" + thresholds = EvaluationThresholds() + + assert thresholds.tool_call_accuracy == 0.5 # Lower threshold for single agent + assert thresholds.groundedness == 0.7 + assert thresholds.relevance == 0.8 + + def test_custom_thresholds(self): + """Test that custom thresholds can be set.""" + thresholds = EvaluationThresholds( + tool_call_accuracy=0.9, + groundedness=0.9, + ) + + assert thresholds.tool_call_accuracy == 0.9 + assert thresholds.groundedness == 0.9 + + def test_strict_thresholds_fail_more(self, sample_test_case: TestCase): + """Test that stricter thresholds cause more failures.""" + response = AgentResponse( + test_case=sample_test_case, + response="Brief response.", + tools_called=["get_customer_detail"], # Only 1 of 2 expected tools + execution_time_ms=1000.0, + ) + + # Default threshold (0.5) - should pass with F1 ~0.67 + default_evaluator = AgentEvaluator(thresholds=EvaluationThresholds()) + default_result = default_evaluator.evaluate_tool_accuracy(response) + + # Strict threshold (0.9) - should fail + strict_evaluator = AgentEvaluator(thresholds=EvaluationThresholds(tool_call_accuracy=0.9)) + strict_result = strict_evaluator.evaluate_tool_accuracy(response) + + # F1 score of ~0.67 passes 0.5 threshold but fails 0.9 + assert default_result["passed"] is True # 0.67 >= 0.5 + assert strict_result["passed"] is False # 0.67 < 0.9 + + +# ============================================================================ +# CLI Runner Test +# ============================================================================ + +@pytest.mark.evaluation +@pytest.mark.unit +def test_cli_can_import(): + """Test that the evaluation module can be imported for CLI use.""" + from evaluation.agent_evaluator import run_evaluation + + assert callable(run_evaluation) From 83db3a99754e53b99cf74e9d4946a94f5a4a24c6 Mon Sep 17 00:00:00 2001 From: "James N." Date: Tue, 3 Feb 2026 13:06:59 -0800 Subject: [PATCH 8/9] add evaluation --- .../multi_agent/INTEGRATION_GUIDE.md | 634 ---------- .../multi_agent/PROJECT_SUMMARY.md | 449 ------- .../multi_agent/QUICK_REFERENCE.md | 351 ------ .../multi_agent/WORKFLOW_DIAGRAMS.md | 337 ----- .../multi_agent/WORKFLOW_REFLECTION_README.md | 345 ----- .../multi_agent/handoff_multi_domain_agent.py | 81 +- .../multi_agent/magentic_group.py | 7 +- .../multi_agent/reflection_agent.py | 76 +- .../multi_agent/reflection_workflow_agent.py | 645 ---------- .../test_reflection_workflow_agent.py | 226 ---- .../agents/agent_framework/single_agent.py | 87 +- agentic_ai/agents/base_agent.py | 92 +- agentic_ai/applications/backend.py | 15 +- agentic_ai/applications/pyproject.toml | 15 +- agentic_ai/applications/uv.lock | 392 +++++- agentic_ai/evaluations/.gitignore | 10 + agentic_ai/evaluations/README.md | 621 +++++++++ agentic_ai/evaluations/__init__.py | 29 + agentic_ai/evaluations/eval_dataset.json | 602 +++++++++ agentic_ai/evaluations/evaluator.py | 458 +++++++ agentic_ai/evaluations/metrics.py | 1106 +++++++++++++++++ agentic_ai/evaluations/run_agent_eval.py | 952 ++++++++++++++ agentic_ai/evaluations/telemetry.py | 61 + 23 files changed, 4531 insertions(+), 3060 deletions(-) delete mode 100644 agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md delete mode 100644 agentic_ai/agents/agent_framework/multi_agent/PROJECT_SUMMARY.md delete mode 100644 agentic_ai/agents/agent_framework/multi_agent/QUICK_REFERENCE.md delete mode 100644 agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md delete mode 100644 agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md delete mode 100644 agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py delete mode 100644 agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py create mode 100644 agentic_ai/evaluations/.gitignore create mode 100644 agentic_ai/evaluations/README.md create mode 100644 agentic_ai/evaluations/__init__.py create mode 100644 agentic_ai/evaluations/eval_dataset.json create mode 100644 agentic_ai/evaluations/evaluator.py create mode 100644 agentic_ai/evaluations/metrics.py create mode 100644 agentic_ai/evaluations/run_agent_eval.py create mode 100644 agentic_ai/evaluations/telemetry.py diff --git a/agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md b/agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md deleted file mode 100644 index ff01f20b1..000000000 --- a/agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md +++ /dev/null @@ -1,634 +0,0 @@ -# Integration Guide: Workflow Reflection Agent - -This guide shows how to integrate the workflow-based reflection agent into your existing application. - -## Quick Start - -### 1. Import the Agent - -```python -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent -``` - -### 2. Basic Integration - -Replace your existing reflection agent import: - -```python -# OLD: Traditional reflection agent -# from agentic_ai.agents.agent_framework.multi_agent.reflection_agent import Agent - -# NEW: Workflow-based reflection agent -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent -``` - -### 3. Use Same Interface - -The workflow agent implements the same `BaseAgent` interface: - -```python -# Create agent instance -state_store = {} -session_id = "user_123" -agent = Agent(state_store=state_store, session_id=session_id) - -# Optional: Set WebSocket manager for streaming -agent.set_websocket_manager(ws_manager) - -# Chat with user -response = await agent.chat_async("Help me with billing for customer 1") -``` - -## Backend Integration (FastAPI/Flask) - -### Example: FastAPI Backend - -```python -from fastapi import FastAPI, WebSocket -from typing import Dict, Any -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - -app = FastAPI() - -# Global state store (in production, use Redis or database) -state_store: Dict[str, Any] = {} - -@app.post("/chat") -async def chat_endpoint( - session_id: str, - message: str, - use_workflow: bool = True # Toggle between traditional and workflow -): - """ - Chat endpoint with workflow reflection agent. - """ - - if use_workflow: - from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - else: - from agentic_ai.agents.agent_framework.multi_agent.reflection_agent import Agent - - # Create agent - agent = Agent(state_store=state_store, session_id=session_id) - - # Process message - response = await agent.chat_async(message) - - return { - "session_id": session_id, - "response": response, - "agent_type": "workflow" if use_workflow else "traditional" - } - - -@app.websocket("/ws/{session_id}") -async def websocket_endpoint(websocket: WebSocket, session_id: str): - """ - WebSocket endpoint for streaming support. - """ - await websocket.accept() - - # Create WebSocket manager (simplified) - class WSManager: - async def broadcast(self, sid: str, message: dict): - if sid == session_id: - await websocket.send_json(message) - - ws_manager = WSManager() - - try: - while True: - # Receive message from client - data = await websocket.receive_json() - message = data.get("message", "") - - # Create agent with streaming support - agent = Agent(state_store=state_store, session_id=session_id) - agent.set_websocket_manager(ws_manager) - - # Process message (will stream updates via WebSocket) - response = await agent.chat_async(message) - - # Send final confirmation - await websocket.send_json({ - "type": "complete", - "response": response - }) - - except Exception as e: - print(f"WebSocket error: {e}") - finally: - await websocket.close() -``` - -## Frontend Integration - -### JavaScript/TypeScript Client - -```typescript -interface ChatMessage { - role: 'user' | 'assistant'; - content: string; -} - -interface StreamEvent { - type: 'orchestrator' | 'agent_start' | 'agent_token' | 'agent_message' | 'final_result'; - agent_id?: string; - content?: string; - kind?: 'plan' | 'progress' | 'result'; -} - -class WorkflowReflectionClient { - private ws: WebSocket; - private sessionId: string; - - constructor(sessionId: string) { - this.sessionId = sessionId; - this.ws = new WebSocket(`ws://localhost:8000/ws/${sessionId}`); - this.setupEventHandlers(); - } - - private setupEventHandlers() { - this.ws.onmessage = (event) => { - const data: StreamEvent = JSON.parse(event.data); - this.handleStreamEvent(data); - }; - } - - private handleStreamEvent(event: StreamEvent) { - switch (event.type) { - case 'orchestrator': - this.updateOrchestrator(event.kind!, event.content!); - break; - - case 'agent_start': - this.showAgentBadge(event.agent_id!); - break; - - case 'agent_token': - this.appendToken(event.agent_id!, event.content!); - break; - - case 'agent_message': - this.finalizeAgentMessage(event.agent_id!, event.content!); - break; - - case 'final_result': - this.displayFinalResponse(event.content!); - break; - } - } - - private updateOrchestrator(kind: string, content: string) { - const container = document.getElementById('orchestrator-status'); - if (container) { - container.innerHTML = ` -
- ${kind.toUpperCase()} -

${content}

-
- `; - } - } - - private showAgentBadge(agentId: string) { - const badge = document.createElement('div'); - badge.className = `agent-badge ${agentId}`; - badge.textContent = agentId.replace('_', ' ').toUpperCase(); - document.getElementById('agent-container')?.appendChild(badge); - } - - private appendToken(agentId: string, token: string) { - const messageDiv = document.getElementById(`message-${agentId}`) - || this.createMessageDiv(agentId); - messageDiv.textContent += token; - } - - private createMessageDiv(agentId: string): HTMLDivElement { - const div = document.createElement('div'); - div.id = `message-${agentId}`; - div.className = 'agent-message streaming'; - document.getElementById('messages-container')?.appendChild(div); - return div; - } - - private finalizeAgentMessage(agentId: string, content: string) { - const messageDiv = document.getElementById(`message-${agentId}`); - if (messageDiv) { - messageDiv.classList.remove('streaming'); - messageDiv.classList.add('complete'); - } - } - - private displayFinalResponse(content: string) { - const responseDiv = document.createElement('div'); - responseDiv.className = 'final-response'; - responseDiv.innerHTML = ` -
- Assistant: -

${content}

-
- `; - document.getElementById('chat-container')?.appendChild(responseDiv); - } - - public sendMessage(message: string) { - this.ws.send(JSON.stringify({ message })); - } -} - -// Usage -const client = new WorkflowReflectionClient('user_session_123'); -client.sendMessage('What is the billing status for customer 1?'); -``` - -### React Component - -```tsx -import React, { useState, useEffect, useCallback } from 'react'; - -interface StreamEvent { - type: string; - agent_id?: string; - content?: string; - kind?: string; -} - -const WorkflowReflectionChat: React.FC<{ sessionId: string }> = ({ sessionId }) => { - const [messages, setMessages] = useState>([]); - const [orchestratorStatus, setOrchestratorStatus] = useState(''); - const [activeAgents, setActiveAgents] = useState>(new Set()); - const [ws, setWs] = useState(null); - - useEffect(() => { - const websocket = new WebSocket(`ws://localhost:8000/ws/${sessionId}`); - - websocket.onmessage = (event) => { - const data: StreamEvent = JSON.parse(event.data); - handleStreamEvent(data); - }; - - setWs(websocket); - - return () => { - websocket.close(); - }; - }, [sessionId]); - - const handleStreamEvent = (event: StreamEvent) => { - switch (event.type) { - case 'orchestrator': - setOrchestratorStatus(event.content || ''); - break; - - case 'agent_start': - setActiveAgents(prev => new Set(prev).add(event.agent_id!)); - break; - - case 'final_result': - setMessages(prev => [...prev, { role: 'assistant', content: event.content! }]); - setActiveAgents(new Set()); - break; - } - }; - - const sendMessage = useCallback((message: string) => { - if (ws && ws.readyState === WebSocket.OPEN) { - ws.send(JSON.stringify({ message })); - setMessages(prev => [...prev, { role: 'user', content: message }]); - } - }, [ws]); - - return ( -
-
- {orchestratorStatus && ( -
- {orchestratorStatus} -
- )} -
- -
- {Array.from(activeAgents).map(agentId => ( - - {agentId.replace('_', ' ')} - - ))} -
- -
- {messages.map((msg, idx) => ( -
- {msg.role}: -

{msg.content}

-
- ))} -
- - -
- ); -}; -``` - -## Streamlit Integration - -```python -import streamlit as st -import asyncio -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - -# Initialize session state -if 'state_store' not in st.session_state: - st.session_state.state_store = {} -if 'session_id' not in st.session_state: - st.session_state.session_id = "streamlit_session" - -# Create agent -@st.cache_resource -def get_agent(): - return Agent( - state_store=st.session_state.state_store, - session_id=st.session_state.session_id - ) - -# UI -st.title("Workflow Reflection Agent Chat") - -# Display chat history -chat_history = st.session_state.state_store.get( - f"{st.session_state.session_id}_chat_history", [] -) - -for msg in chat_history: - with st.chat_message(msg["role"]): - st.write(msg["content"]) - -# Chat input -if prompt := st.chat_input("Ask me anything..."): - # Display user message - with st.chat_message("user"): - st.write(prompt) - - # Get agent response - agent = get_agent() - - # Show processing indicator - with st.spinner("Processing with workflow reflection..."): - response = asyncio.run(agent.chat_async(prompt)) - - # Display assistant response - with st.chat_message("assistant"): - st.write(response) - - # Rerun to update chat history - st.rerun() -``` - -## Configuration Management - -### Environment Configuration - -Create a `.env` file: - -```bash -# Azure OpenAI Configuration -AZURE_OPENAI_API_KEY=your_api_key_here -AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4 -AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/ -AZURE_OPENAI_API_VERSION=2024-02-15-preview -OPENAI_MODEL_NAME=gpt-4 - -# Optional: MCP Server -MCP_SERVER_URI=http://localhost:5000/mcp -``` - -### Dynamic Agent Selection - -```python -from typing import Literal -from agentic_ai.agents.base_agent import BaseAgent - -AgentType = Literal["workflow", "traditional"] - -def create_agent( - agent_type: AgentType, - state_store: dict, - session_id: str, - **kwargs -) -> BaseAgent: - """ - Factory function to create the appropriate agent type. - """ - if agent_type == "workflow": - from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - elif agent_type == "traditional": - from agentic_ai.agents.agent_framework.multi_agent.reflection_agent import Agent - else: - raise ValueError(f"Unknown agent type: {agent_type}") - - return Agent(state_store=state_store, session_id=session_id, **kwargs) - -# Usage -agent = create_agent( - agent_type="workflow", # or "traditional" - state_store=state_store, - session_id=session_id, - access_token=access_token -) -``` - -## Monitoring and Logging - -### Enhanced Logging - -```python -import logging -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - -# Configure detailed logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.FileHandler('workflow_agent.log'), - logging.StreamHandler() - ] -) - -# Create agent -agent = Agent(state_store=state_store, session_id=session_id) - -# Use agent (logs will capture all workflow steps) -response = await agent.chat_async("Help me") -``` - -### Metrics Collection - -```python -import time -from dataclasses import dataclass -from typing import List - -@dataclass -class WorkflowMetrics: - session_id: str - request_id: str - start_time: float - end_time: float - refinement_count: int - approved: bool - - @property - def duration(self) -> float: - return self.end_time - self.start_time - -class MetricsCollector: - def __init__(self): - self.metrics: List[WorkflowMetrics] = [] - - def track_request(self, session_id: str, request_id: str): - # Implementation for tracking metrics - pass - - def report(self): - total_requests = len(self.metrics) - avg_duration = sum(m.duration for m in self.metrics) / total_requests - avg_refinements = sum(m.refinement_count for m in self.metrics) / total_requests - - print(f"Total Requests: {total_requests}") - print(f"Average Duration: {avg_duration:.2f}s") - print(f"Average Refinements: {avg_refinements:.2f}") - -# Usage with agent -metrics = MetricsCollector() -# Integrate with agent workflow -``` - -## Testing - -### Unit Tests - -```python -import pytest -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - -@pytest.fixture -def agent(): - state_store = {} - return Agent(state_store=state_store, session_id="test_session") - -@pytest.mark.asyncio -async def test_basic_chat(agent): - response = await agent.chat_async("What is 2+2?") - assert response is not None - assert len(response) > 0 - -@pytest.mark.asyncio -async def test_conversation_history(agent): - # First message - await agent.chat_async("My name is John") - - # Second message should have context - response = await agent.chat_async("What is my name?") - assert "john" in response.lower() - -@pytest.mark.asyncio -async def test_mcp_tool_usage(agent): - # Assuming MCP is configured - response = await agent.chat_async("Get customer details for ID 1") - # Verify tool was used and response contains customer data - assert "customer" in response.lower() -``` - -### Integration Tests - -```python -import pytest -from fastapi.testclient import TestClient -from your_backend import app - -@pytest.fixture -def client(): - return TestClient(app) - -def test_chat_endpoint(client): - response = client.post( - "/chat", - json={ - "session_id": "test_123", - "message": "Hello", - "use_workflow": True - } - ) - assert response.status_code == 200 - data = response.json() - assert data["agent_type"] == "workflow" - assert "response" in data -``` - -## Best Practices - -1. **Session Management**: Use unique session IDs per user -2. **State Persistence**: Store state in Redis/database for production -3. **Error Handling**: Implement proper error boundaries -4. **Rate Limiting**: Protect endpoints from abuse -5. **Authentication**: Secure MCP endpoints with proper tokens -6. **Monitoring**: Log all workflow events for debugging -7. **Testing**: Write comprehensive tests for edge cases - -## Troubleshooting - -### Issue: Workflow hangs - -**Cause**: Missing message handlers or unconnected edges - -**Solution**: Verify WorkflowBuilder has all necessary edges: -```python -.add_edge(primary_agent, reviewer_agent) -.add_edge(reviewer_agent, primary_agent) -``` - -### Issue: MCP tools not working - -**Cause**: MCP_SERVER_URI not set or server not running - -**Solution**: -```bash -# Start MCP server -python mcp/mcp_service.py - -# Set environment variable -export MCP_SERVER_URI=http://localhost:5000/mcp -``` - -### Issue: Streaming not working - -**Cause**: WebSocket manager not set - -**Solution**: -```python -agent.set_websocket_manager(ws_manager) -``` - -## Migration Checklist - -- [ ] Update agent imports -- [ ] Test basic chat functionality -- [ ] Verify conversation history persistence -- [ ] Test streaming with WebSocket -- [ ] Validate MCP tool integration -- [ ] Update frontend to handle new event types -- [ ] Configure monitoring and logging -- [ ] Run integration tests -- [ ] Deploy to staging environment -- [ ] Monitor performance metrics - -## Support - -For issues or questions: -1. Check the [README](WORKFLOW_REFLECTION_README.md) -2. Review [Architecture Diagrams](WORKFLOW_DIAGRAMS.md) -3. Run tests: `python test_reflection_workflow_agent.py` -4. Enable debug logging for detailed traces diff --git a/agentic_ai/agents/agent_framework/multi_agent/PROJECT_SUMMARY.md b/agentic_ai/agents/agent_framework/multi_agent/PROJECT_SUMMARY.md deleted file mode 100644 index 752e0f928..000000000 --- a/agentic_ai/agents/agent_framework/multi_agent/PROJECT_SUMMARY.md +++ /dev/null @@ -1,449 +0,0 @@ -# Workflow-Based Reflection Agent - Project Summary - -## What We Created - -A complete workflow-based implementation of the reflection agent pattern using Agent Framework's `WorkflowBuilder`, featuring a 3-party communication design with quality assurance gates. - -## Files Created - -### 1. **reflection_workflow_agent.py** (Main Implementation) -Location: `agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py` - -**Key Components:** -- `PrimaryAgentExecutor`: Customer support agent with MCP tool support -- `ReviewerAgentExecutor`: Quality assurance gate with conditional routing -- `Agent`: Main class implementing `BaseAgent` interface - -**Features:** -- ✅ 3-party communication pattern (User → Primary → Reviewer → User) -- ✅ Conversation history management -- ✅ MCP tool integration -- ✅ Streaming support via WebSocket -- ✅ Iterative refinement with feedback loops -- ✅ Compatible with existing `BaseAgent` interface - -### 2. **test_reflection_workflow_agent.py** (Test Suite) -Location: `agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py` - -**Features:** -- Environment variable validation -- Basic chat functionality tests -- MCP tool integration tests -- Conversation history verification -- User-friendly output with progress indicators - -**Usage:** -```bash -python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py -``` - -### 3. **WORKFLOW_REFLECTION_README.md** (Documentation) -Location: `agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md` - -**Contents:** -- Architecture overview -- 3-party communication pattern explanation -- Implementation details -- Usage examples -- Environment configuration -- Troubleshooting guide -- Comparison with traditional approach -- Best practices - -### 4. **WORKFLOW_DIAGRAMS.md** (Visual Documentation) -Location: `agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md` - -**Mermaid Diagrams:** -- 3-party communication flow -- Detailed workflow execution sequence -- Message type relationships -- Workflow graph structure -- State management flow -- Conversation history flow -- Traditional vs Workflow comparison -- MCP tool integration -- Error handling flow -- Streaming events flow - -### 5. **INTEGRATION_GUIDE.md** (Integration Documentation) -Location: `agentic_ai/agents/agent_framework/multi_agent/INTEGRATION_GUIDE.md` - -**Contents:** -- Quick start guide -- Backend integration (FastAPI example) -- Frontend integration (JavaScript/TypeScript, React) -- Streamlit integration -- Configuration management -- Monitoring and logging -- Testing strategies -- Migration checklist - -## Architecture Highlights - -### 3-Party Communication Pattern - -``` -User → PrimaryAgent → ReviewerAgent → {approve: User, reject: PrimaryAgent} - ↑ | - |__________________________________________| - (feedback loop) -``` - -**Key Principles:** -1. PrimaryAgent receives user messages but cannot send directly to user -2. All PrimaryAgent outputs go to ReviewerAgent -3. ReviewerAgent acts as conditional gate (approve/reject) -4. Conversation history maintained between User and PrimaryAgent only -5. Both agents receive history for context - -### Workflow Graph - -```python -workflow = ( - WorkflowBuilder() - .add_edge(primary_agent, reviewer_agent) # Forward path - .add_edge(reviewer_agent, primary_agent) # Feedback path - .set_start_executor(primary_agent) - .build() - .as_agent() -) -``` - -### Message Types - -1. **PrimaryAgentRequest**: User → PrimaryAgent - - `request_id`: Unique identifier - - `user_prompt`: User's question - - `conversation_history`: Previous messages - -2. **ReviewRequest**: PrimaryAgent → ReviewerAgent - - `request_id`: Same as original request - - `user_prompt`: Original question - - `conversation_history`: For context - - `primary_agent_response`: Agent's answer - -3. **ReviewResponse**: ReviewerAgent → PrimaryAgent - - `request_id`: Correlation ID - - `approved`: Boolean decision - - `feedback`: Constructive feedback or approval note - -## Key Features - -### ✅ Workflow-Based Architecture -- Built using `WorkflowBuilder` for explicit control flow -- Bidirectional edges between executors -- Conditional routing based on structured decisions - -### ✅ Quality Assurance -- Every response reviewed before reaching user -- Structured evaluation criteria: - - Accuracy of information - - Completeness of answer - - Professional tone - - Proper tool usage - - Clarity and helpfulness - -### ✅ Iterative Refinement -- Failed reviews trigger regeneration with feedback -- Conversation context preserved across iterations -- Unlimited refinement cycles until approval - -### ✅ MCP Tool Integration -- Supports MCP tools for external data access -- Tools available to both agents -- Proper authentication via bearer tokens - -### ✅ Streaming Support -- WebSocket-based streaming for real-time updates -- Progress indicators for each workflow stage -- Token-level streaming for agent responses - -### ✅ State Management -- Conversation history persisted in state store -- Session-based isolation -- Compatible with Redis/database for production - -## Usage Examples - -### Basic Usage - -```python -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - -# Create agent -state_store = {} -agent = Agent(state_store=state_store, session_id="user_123") - -# Chat -response = await agent.chat_async("Help with customer 1") -``` - -### With Streaming - -```python -# Set WebSocket manager -agent.set_websocket_manager(ws_manager) - -# Chat with streaming updates -response = await agent.chat_async("What promotions are available?") -``` - -### With MCP Tools - -```python -# Set MCP_SERVER_URI environment variable -os.environ["MCP_SERVER_URI"] = "http://localhost:5000/mcp" - -# Agent will automatically use MCP tools -agent = Agent(state_store=state_store, session_id="user_123", access_token=token) -response = await agent.chat_async("Get billing summary for customer 1") -``` - -## Comparison: Workflow vs Traditional - -| Feature | Traditional | Workflow | -|---------|------------|----------| -| **Architecture** | Sequential agent.run() calls | Message-based graph execution | -| **Control Flow** | Implicit (procedural code) | Explicit (workflow edges) | -| **State Management** | Manual (instance variables) | Framework-managed | -| **Scalability** | Limited | Highly scalable | -| **Testing** | Mock agent methods | Mock message handlers | -| **Debugging** | Step through code | Trace message flow | -| **Extensibility** | Modify agent code | Add executors/edges | - -## Integration Points - -### Backend Integration -- ✅ FastAPI example provided -- ✅ WebSocket support for streaming -- ✅ Compatible with existing BaseAgent interface -- ✅ No breaking changes to API - -### Frontend Integration -- ✅ JavaScript/TypeScript client example -- ✅ React component example -- ✅ Stream event handlers -- ✅ Progressive UI updates - -### Streamlit Integration -- ✅ Complete Streamlit example -- ✅ Session state management -- ✅ Chat history display -- ✅ Async execution handling - -## Testing - -### Run Tests - -```bash -# Basic test -python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py - -# With specific Python -python3.11 agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py -``` - -### Test Coverage -- ✅ Environment validation -- ✅ Basic chat functionality -- ✅ Conversation history -- ✅ MCP tool integration -- ✅ Error handling - -## Environment Variables - -**Required:** -- `AZURE_OPENAI_API_KEY` -- `AZURE_OPENAI_CHAT_DEPLOYMENT` -- `AZURE_OPENAI_ENDPOINT` -- `AZURE_OPENAI_API_VERSION` -- `OPENAI_MODEL_NAME` - -**Optional:** -- `MCP_SERVER_URI` (enables MCP tool usage) - -## Documentation Structure - -``` -agentic_ai/agents/agent_framework/multi_agent/ -├── reflection_workflow_agent.py # Main implementation -├── test_reflection_workflow_agent.py # Test suite -├── WORKFLOW_REFLECTION_README.md # Main documentation -├── WORKFLOW_DIAGRAMS.md # Visual diagrams -├── INTEGRATION_GUIDE.md # Integration examples -└── PROJECT_SUMMARY.md # This file -``` - -## Key Learnings from Reference Examples - -### From `workflow_as_agent_reflection_pattern_azure.py` -- ✅ WorkflowBuilder usage patterns -- ✅ Message-based communication -- ✅ AgentRunUpdateEvent for output emission -- ✅ Structured output with Pydantic - -### From `workflow_as_agent_human_in_the_loop_azure.py` -- ✅ RequestInfoExecutor pattern -- ✅ Correlation with request IDs -- ✅ Bidirectional edge configuration - -### From `edge_condition.py` -- ✅ Conditional routing with predicates -- ✅ Boolean edge conditions -- ✅ Structured decision parsing - -### From `guessing_game_with_human_input.py` -- ✅ Event-driven architecture -- ✅ RequestResponse correlation -- ✅ Typed request payloads - -## Advantages of Workflow Approach - -### 1. **Explicit Control Flow** -Workflow edges make the communication pattern crystal clear: -```python -.add_edge(primary_agent, reviewer_agent) -.add_edge(reviewer_agent, primary_agent) -``` - -### 2. **Better Separation of Concerns** -Each executor has a single responsibility: -- PrimaryAgent: Generate responses -- ReviewerAgent: Evaluate quality - -### 3. **Framework-Managed State** -No need to manually track pending requests across retries. - -### 4. **Easier Testing** -Mock message handlers instead of complex agent interactions. - -### 5. **Scalability** -Easy to add more executors (e.g., specialized reviewers, human escalation). - -### 6. **Debugging** -Message flow is traceable through logs. - -## Future Enhancement Ideas - -### Short Term -- [ ] Add max refinement limit to prevent infinite loops -- [ ] Implement retry logic with exponential backoff -- [ ] Add metrics collection for performance monitoring -- [ ] Create Jupyter notebook examples - -### Medium Term -- [ ] Support parallel reviewer agents (consensus-based approval) -- [ ] Add human-in-the-loop escalation for edge cases -- [ ] Implement A/B testing framework for review criteria -- [ ] Create dashboard for workflow analytics - -### Long Term -- [ ] Multi-modal support (images, files) -- [ ] Fine-tuned reviewer models -- [ ] Dynamic workflow routing based on request type -- [ ] Integration with external approval systems - -## Migration from Traditional Agent - -### Step-by-Step Migration - -1. **Update Import** - ```python - # OLD - from agentic_ai.agents.agent_framework.multi_agent.reflection_agent import Agent - - # NEW - from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - ``` - -2. **No Code Changes Required** - The workflow agent implements the same `BaseAgent` interface. - -3. **Test Thoroughly** - Run integration tests to verify behavior. - -4. **Monitor Performance** - Compare response times and quality metrics. - -5. **Gradual Rollout** - Use feature flags to gradually migrate users. - -### Migration Checklist - -- [ ] Update agent imports -- [ ] Test basic chat functionality -- [ ] Verify conversation history -- [ ] Test streaming with WebSocket -- [ ] Validate MCP tool integration -- [ ] Update frontend event handlers -- [ ] Configure monitoring -- [ ] Run integration tests -- [ ] Deploy to staging -- [ ] Monitor metrics -- [ ] Full production rollout - -## Success Criteria - -### Functional Requirements -- ✅ All responses reviewed before delivery -- ✅ Conversation history maintained correctly -- ✅ MCP tools work as expected -- ✅ Streaming updates work properly -- ✅ Compatible with existing interface - -### Non-Functional Requirements -- ✅ Response time < 5 seconds (typical) -- ✅ Clear logging for debugging -- ✅ Proper error handling -- ✅ Comprehensive documentation -- ✅ Test coverage > 80% - -## Resources - -### Documentation -- [Main README](WORKFLOW_REFLECTION_README.md) -- [Architecture Diagrams](WORKFLOW_DIAGRAMS.md) -- [Integration Guide](INTEGRATION_GUIDE.md) - -### Code -- [Implementation](reflection_workflow_agent.py) -- [Tests](test_reflection_workflow_agent.py) - -### References -- [Agent Framework Reflection Example](../../../reference/agent-framework/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern_azure.py) -- [Human-in-the-Loop Example](../../../reference/agent-framework/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop_azure.py) -- [Edge Conditions Example](../../../reference/agent-framework/python/samples/getting_started/workflows/control-flow/edge_condition.py) - -## Support and Feedback - -For issues, questions, or feedback: - -1. **Check Documentation**: Review README and integration guide -2. **Run Tests**: Execute test suite to validate setup -3. **Enable Debug Logging**: Set log level to DEBUG -4. **Review Diagrams**: Check architecture diagrams for understanding -5. **Create Issue**: Document issue with logs and reproduction steps - -## Conclusion - -The workflow-based reflection agent provides a robust, scalable, and maintainable implementation of the reflection pattern. It leverages Agent Framework's workflow capabilities to create an explicit, testable, and extensible architecture that's ready for production use. - -**Key Benefits:** -- ✅ Explicit 3-party communication pattern -- ✅ Quality-assured responses -- ✅ Iterative refinement -- ✅ Production-ready with streaming -- ✅ Fully compatible with existing system -- ✅ Comprehensive documentation - -**Ready to Use:** -- All code tested and documented -- Integration examples provided -- Migration path clear -- Support materials available - ---- - -**Version**: 1.0.0 -**Date**: October 2025 -**Status**: Production Ready ✅ diff --git a/agentic_ai/agents/agent_framework/multi_agent/QUICK_REFERENCE.md b/agentic_ai/agents/agent_framework/multi_agent/QUICK_REFERENCE.md deleted file mode 100644 index 2f7e1a7d8..000000000 --- a/agentic_ai/agents/agent_framework/multi_agent/QUICK_REFERENCE.md +++ /dev/null @@ -1,351 +0,0 @@ -# Workflow Reflection Agent - Quick Reference - -## One-Minute Overview - -**What**: Workflow-based reflection agent with 3-party quality assurance pattern -**When**: Use for high-quality responses with built-in review process -**Why**: Better control flow, scalability, and maintainability vs traditional approach - -## Quick Start (30 seconds) - -```python -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - -state_store = {} -agent = Agent(state_store=state_store, session_id="user_123") -response = await agent.chat_async("Your question here") -``` - -## Architecture at a Glance - -``` -User ─┬──> PrimaryAgent ─┬──> ReviewerAgent ─┬──> User (if approved) - │ │ │ - └─ History ─────────┘ └──> PrimaryAgent (if rejected) - │ - └──> (loop) -``` - -## Key Files - -| File | Purpose | Size | -|------|---------|------| -| `reflection_workflow_agent.py` | Main implementation | ~600 lines | -| `test_reflection_workflow_agent.py` | Test suite | ~200 lines | -| `WORKFLOW_REFLECTION_README.md` | Full documentation | ~400 lines | -| `WORKFLOW_DIAGRAMS.md` | Visual diagrams | ~500 lines | -| `INTEGRATION_GUIDE.md` | Integration examples | ~800 lines | - -## Message Flow Cheat Sheet - -### 1️⃣ User → PrimaryAgent -```python -PrimaryAgentRequest( - request_id=uuid4(), - user_prompt="Help me", - conversation_history=[...] -) -``` - -### 2️⃣ PrimaryAgent → ReviewerAgent -```python -ReviewRequest( - request_id=request_id, - user_prompt="Help me", - conversation_history=[...], - primary_agent_response=[ChatMessage(...)] -) -``` - -### 3️⃣ ReviewerAgent Decision -```python -ReviewDecision( - approved=True/False, - feedback="..." -) -``` - -### 4️⃣ Output -- **If approved**: `AgentRunUpdateEvent` → User -- **If rejected**: `ReviewResponse` → PrimaryAgent (loop to step 2) - -## Common Tasks - -### Enable Streaming -```python -agent.set_websocket_manager(ws_manager) -``` - -### Enable MCP Tools -```bash -export MCP_SERVER_URI=http://localhost:5000/mcp -``` - -### Access History -```python -history = agent.chat_history # List of dicts -# or -history = agent._conversation_history # List of ChatMessage -``` - -### Run Tests -```bash -python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py -``` - -## Environment Variables - -```bash -# Required -AZURE_OPENAI_API_KEY=sk-... -AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-4 -AZURE_OPENAI_ENDPOINT=https://....openai.azure.com/ -AZURE_OPENAI_API_VERSION=2024-02-15-preview -OPENAI_MODEL_NAME=gpt-4 - -# Optional -MCP_SERVER_URI=http://localhost:5000/mcp -``` - -## Streaming Events - -| Event Type | When | Purpose | -|------------|------|---------| -| `orchestrator` | Start/Progress/End | Workflow status | -| `agent_start` | Agent begins | Show agent badge | -| `agent_token` | Token generated | Stream text | -| `agent_message` | Agent completes | Full message | -| `tool_called` | Tool invoked | Show tool usage | -| `final_result` | Workflow done | Final response | - -## Debug Checklist - -❓ **Not working?** -1. Check environment variables are set -2. Verify MCP server is running (if using tools) -3. Enable debug logging: `logging.basicConfig(level=logging.DEBUG)` -4. Check WebSocket manager is set (for streaming) -5. Review logs for error messages - -❓ **Infinite loop?** -1. Check reviewer criteria are achievable -2. Add max refinement counter -3. Review feedback content for clarity - -❓ **No MCP tools?** -1. Verify `MCP_SERVER_URI` is set -2. Test MCP server: `curl $MCP_SERVER_URI/health` -3. Check access token is valid - -## Comparison Matrix - -| Feature | Traditional | Workflow | Winner | -|---------|------------|----------|--------| -| Control Flow | Implicit | Explicit | 🏆 Workflow | -| Testability | Medium | High | 🏆 Workflow | -| Scalability | Limited | High | 🏆 Workflow | -| Learning Curve | Low | Medium | 🥈 Traditional | -| State Management | Manual | Auto | 🏆 Workflow | -| Debugging | Hard | Easy | 🏆 Workflow | - -## Code Snippets - -### Backend Integration (FastAPI) -```python -@app.post("/chat") -async def chat(session_id: str, message: str): - agent = Agent(state_store, session_id) - response = await agent.chat_async(message) - return {"response": response} -``` - -### Frontend Integration (React) -```tsx -const [response, setResponse] = useState(''); -ws.onmessage = (event) => { - const data = JSON.parse(event.data); - if (data.type === 'final_result') { - setResponse(data.content); - } -}; -``` - -### Streamlit Integration -```python -agent = Agent(st.session_state.state_store, session_id) -if prompt := st.chat_input("Ask..."): - response = asyncio.run(agent.chat_async(prompt)) - st.chat_message("assistant").write(response) -``` - -## Performance Tips - -✅ **DO:** -- Use streaming for better UX -- Enable debug logging during development -- Implement retry logic for MCP tools -- Cache frequent queries -- Monitor refinement counts - -❌ **DON'T:** -- Allow unlimited refinement loops -- Log sensitive customer data -- Skip error handling -- Forget to persist state -- Ignore WebSocket errors - -## Workflow Builder Pattern - -```python -workflow = ( - WorkflowBuilder() - .add_edge(executor_a, executor_b) # A → B - .add_edge(executor_b, executor_a) # B → A (feedback) - .set_start_executor(executor_a) # Start with A - .build() # Build workflow - .as_agent() # Expose as agent -) -``` - -## Executor Handlers - -```python -class MyExecutor(Executor): - @handler - async def handle_message( - self, - request: RequestType, - ctx: WorkflowContext[ResponseType] - ) -> None: - # Process request - result = await self.process(request) - - # Send to next executor - await ctx.send_message(result) - - # Or emit to user - await ctx.add_event( - AgentRunUpdateEvent( - self.id, - data=AgentRunResponseUpdate(...) - ) - ) -``` - -## Structured Output - -```python -from pydantic import BaseModel - -class MyResponse(BaseModel): - field1: str - field2: bool - -# Use in chat client -response = await chat_client.get_response( - messages=[...], - response_format=MyResponse -) - -# Parse -parsed = MyResponse.model_validate_json(response.text) -``` - -## Logging Best Practices - -```python -import logging - -logger = logging.getLogger(__name__) - -# In executor -logger.info(f"[{self.id}] Processing request {request_id[:8]}") -logger.debug(f"[{self.id}] Full request: {request}") -logger.error(f"[{self.id}] Error: {e}", exc_info=True) -``` - -## Testing Patterns - -```python -@pytest.fixture -def agent(): - return Agent(state_store={}, session_id="test") - -@pytest.mark.asyncio -async def test_chat(agent): - response = await agent.chat_async("Hello") - assert response is not None - assert len(response) > 0 - -@pytest.mark.asyncio -async def test_history(agent): - await agent.chat_async("My name is John") - response = await agent.chat_async("What is my name?") - assert "john" in response.lower() -``` - -## Common Pitfalls - -🔴 **Pitfall 1**: Not setting start executor -```python -# Wrong -WorkflowBuilder().add_edge(a, b).build() - -# Right -WorkflowBuilder().add_edge(a, b).set_start_executor(a).build() -``` - -🔴 **Pitfall 2**: Missing return edges -```python -# Wrong (one-way only) -.add_edge(primary, reviewer) - -# Right (bidirectional for loops) -.add_edge(primary, reviewer) -.add_edge(reviewer, primary) -``` - -🔴 **Pitfall 3**: Not handling async properly -```python -# Wrong -response = agent.chat_async(prompt) - -# Right -response = await agent.chat_async(prompt) -# or -response = asyncio.run(agent.chat_async(prompt)) -``` - -## Links - -📚 **Documentation** -- [Full README](WORKFLOW_REFLECTION_README.md) -- [Diagrams](WORKFLOW_DIAGRAMS.md) -- [Integration Guide](INTEGRATION_GUIDE.md) -- [Project Summary](PROJECT_SUMMARY.md) - -🔧 **Code** -- [Implementation](reflection_workflow_agent.py) -- [Tests](test_reflection_workflow_agent.py) - -📖 **Examples** -- Agent Framework Samples in `reference/agent-framework/` - -## Support - -1. Check docs ↑ -2. Run tests -3. Enable debug logging -4. Review error messages -5. Check environment vars - -## Version Info - -- **Version**: 1.0.0 -- **Status**: ✅ Production Ready -- **Python**: 3.10+ -- **Dependencies**: agent-framework, pydantic, azure-identity - ---- - -**TIP**: Bookmark this page for quick reference! 📌 diff --git a/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md b/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md deleted file mode 100644 index 065a5f1e2..000000000 --- a/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_DIAGRAMS.md +++ /dev/null @@ -1,337 +0,0 @@ -# Workflow-Based Reflection Agent - Architecture Diagrams - -## 3-Party Communication Flow - -```mermaid -graph TD - User[User] -->|PrimaryAgentRequest| PA[PrimaryAgent Executor] - PA -->|ReviewRequest| RA[ReviewerAgent Executor] - RA -->|ReviewResponse approved=false| PA - RA -->|AgentRunUpdateEvent approved=true| User - - style User fill:#e1f5ff - style PA fill:#fff4e1 - style RA fill:#e8f5e8 -``` - -## Detailed Workflow Execution - -```mermaid -sequenceDiagram - participant User - participant WorkflowAgent - participant PrimaryAgent - participant ReviewerAgent - - User->>WorkflowAgent: chat_async("Help with customer 1") - WorkflowAgent->>PrimaryAgent: PrimaryAgentRequest
(prompt + history) - - Note over PrimaryAgent: Generate response
using MCP tools
and conversation history - - PrimaryAgent->>ReviewerAgent: ReviewRequest
(prompt + history + response) - - Note over ReviewerAgent: Evaluate response quality
Check accuracy, completeness,
professionalism - - alt Response Approved - ReviewerAgent->>ReviewerAgent: AgentRunUpdateEvent - ReviewerAgent->>WorkflowAgent: Emit to user - WorkflowAgent->>User: Final response - else Response Rejected - ReviewerAgent->>PrimaryAgent: ReviewResponse
(approved=false, feedback) - - Note over PrimaryAgent: Incorporate feedback
Regenerate response - - PrimaryAgent->>ReviewerAgent: ReviewRequest
(refined response) - - Note over ReviewerAgent: Re-evaluate - - ReviewerAgent->>ReviewerAgent: AgentRunUpdateEvent - ReviewerAgent->>WorkflowAgent: Emit to user - WorkflowAgent->>User: Final response - end -``` - -## Message Types - -```mermaid -classDiagram - class PrimaryAgentRequest { - +str request_id - +str user_prompt - +list~ChatMessage~ conversation_history - } - - class ReviewRequest { - +str request_id - +str user_prompt - +list~ChatMessage~ conversation_history - +list~ChatMessage~ primary_agent_response - } - - class ReviewResponse { - +str request_id - +bool approved - +str feedback - } - - class ReviewDecision { - +bool approved - +str feedback - } - - PrimaryAgentRequest --> ReviewRequest : transforms to - ReviewRequest --> ReviewDecision : evaluates into - ReviewDecision --> ReviewResponse : converts to - ReviewResponse --> PrimaryAgentRequest : triggers retry if rejected -``` - -## Workflow Graph Structure - -```mermaid -graph LR - Start([Start]) --> PA[PrimaryAgent
Executor] - PA -->|ReviewRequest| RA[ReviewerAgent
Executor] - RA -->|ReviewResponse
approved=false| PA - RA -->|AgentRunUpdateEvent
approved=true| End([User]) - - style Start fill:#90EE90 - style End fill:#FFB6C1 - style PA fill:#FFE4B5 - style RA fill:#E0BBE4 -``` - -## State Management - -```mermaid -stateDiagram-v2 - [*] --> UserInput: User sends prompt - - UserInput --> PrimaryGenerate: Create PrimaryAgentRequest
with conversation history - - PrimaryGenerate --> ReviewEvaluate: Send ReviewRequest
to ReviewerAgent - - ReviewEvaluate --> Approved: Quality check passes - ReviewEvaluate --> Rejected: Quality check fails - - Rejected --> PrimaryRefinement: Send ReviewResponse
with feedback - - PrimaryRefinement --> ReviewEvaluate: Send refined ReviewRequest - - Approved --> EmitToUser: AgentRunUpdateEvent - - EmitToUser --> UpdateHistory: Add to conversation history - - UpdateHistory --> [*]: Return response to user - - note right of ReviewEvaluate - Conditional Gate: - - Accuracy - - Completeness - - Professionalism - - Tool usage - - Clarity - end note - - note right of PrimaryRefinement - Incorporate feedback: - - Add reviewer feedback to context - - Regenerate response - - Maintain conversation history - end note -``` - -## Conversation History Flow - -```mermaid -graph TB - subgraph "State Store" - History[Conversation History
User ↔ PrimaryAgent only] - end - - subgraph "Request 1" - U1[User: Query 1] --> P1[PrimaryAgent] - P1 --> R1[ReviewerAgent] - R1 -->|approved| H1[Add to History] - H1 --> History - end - - subgraph "Request 2 with History" - History --> P2[PrimaryAgent
receives history] - U2[User: Query 2] --> P2 - P2 --> R2[ReviewerAgent
receives history] - R2 -->|approved| H2[Add to History] - H2 --> History - end - - style History fill:#FFE4E1 - style H1 fill:#90EE90 - style H2 fill:#90EE90 -``` - -## Comparison: Traditional vs Workflow - -```mermaid -graph TB - subgraph "Traditional Reflection Agent" - T1[Agent.run Step 1:
Primary generates] --> T2[Agent.run Step 2:
Reviewer evaluates] - T2 --> T3{Approved?} - T3 -->|No| T4[Agent.run Step 3:
Primary refines] - T4 --> T2 - T3 -->|Yes| T5[Return to user] - - style T1 fill:#FFE4B5 - style T2 fill:#E0BBE4 - style T4 fill:#FFE4B5 - end - - subgraph "Workflow Reflection Agent" - W1[PrimaryAgentExecutor
handles request] --> W2[ReviewerAgentExecutor
evaluates] - W2 --> W3{Approved?} - W3 -->|No| W4[PrimaryAgentExecutor
handles feedback] - W4 --> W2 - W3 -->|Yes| W5[AgentRunUpdateEvent
to user] - - style W1 fill:#FFE4B5 - style W2 fill:#E0BBE4 - style W4 fill:#FFE4B5 - style W5 fill:#90EE90 - end -``` - -## MCP Tool Integration - -```mermaid -graph LR - subgraph "Workflow" - PA[PrimaryAgent] --> RA[ReviewerAgent] - RA --> PA - end - - subgraph "MCP Tools" - T1[get_customer_detail] - T2[get_billing_summary] - T3[get_promotions] - T4[search_knowledge_base] - end - - PA -.->|Uses tools| T1 - PA -.->|Uses tools| T2 - PA -.->|Uses tools| T3 - PA -.->|Uses tools| T4 - - RA -.->|May use tools
to verify| T1 - - subgraph "MCP Server" - MCP[HTTP MCP Server
:5000/mcp] - end - - T1 --> MCP - T2 --> MCP - T3 --> MCP - T4 --> MCP - - style PA fill:#FFE4B5 - style RA fill:#E0BBE4 - style MCP fill:#E1F5FF -``` - -## Error Handling Flow - -```mermaid -graph TD - Start([User Query]) --> Init[Initialize Workflow] - - Init --> CheckEnv{Env Config OK?} - CheckEnv -->|No| Error1[Raise RuntimeError] - CheckEnv -->|Yes| CreateReq[Create PrimaryAgentRequest] - - CreateReq --> PA[PrimaryAgent Process] - - PA --> CheckPA{Primary Success?} - CheckPA -->|Error| Error2[Log error + Raise] - CheckPA -->|Success| RA[ReviewerAgent Process] - - RA --> CheckRA{Review Success?} - CheckRA -->|Error| Error3[Log error + Raise] - CheckRA -->|Success| Decision{Approved?} - - Decision -->|Yes| Success[Return to User] - Decision -->|No| CheckRetry{Max Retries?} - - CheckRetry -->|Exceeded| Error4[Log warning + Return best attempt] - CheckRetry -->|Continue| PA - - style Error1 fill:#FFB6C1 - style Error2 fill:#FFB6C1 - style Error3 fill:#FFB6C1 - style Error4 fill:#FFE4B5 - style Success fill:#90EE90 -``` - -## Streaming Events Flow - -```mermaid -sequenceDiagram - participant User - participant Backend - participant WorkflowAgent - participant WebSocket - - User->>Backend: Send query - Backend->>WorkflowAgent: chat_async(query) - - WorkflowAgent->>WebSocket: orchestrator: "plan"
"Workflow starting..." - WebSocket->>User: Display plan - - WorkflowAgent->>WebSocket: agent_start: "primary_agent" - WebSocket->>User: Show agent badge - - loop Primary Generation - WorkflowAgent->>WebSocket: agent_token: chunk - WebSocket->>User: Stream text - end - - WorkflowAgent->>WebSocket: agent_message: complete - WebSocket->>User: Display message - - WorkflowAgent->>WebSocket: orchestrator: "progress"
"Reviewer evaluating..." - WebSocket->>User: Update progress - - WorkflowAgent->>WebSocket: agent_start: "reviewer_agent" - WebSocket->>User: Show reviewer badge - - loop Reviewer Evaluation - WorkflowAgent->>WebSocket: agent_token: chunk - WebSocket->>User: Stream text - end - - alt Approved - WorkflowAgent->>WebSocket: orchestrator: "result"
"Approved!" - WorkflowAgent->>WebSocket: final_result: response - WebSocket->>User: Display final response - else Rejected - WorkflowAgent->>WebSocket: orchestrator: "progress"
"Refining..." - Note over WorkflowAgent: Loop back to Primary - end -``` - ---- - -## How to View These Diagrams - -These diagrams use Mermaid syntax, which is supported by: - -1. **GitHub**: Automatically rendered in Markdown files -2. **VS Code**: Install "Markdown Preview Mermaid Support" extension -3. **Online**: Copy to https://mermaid.live -4. **Documentation sites**: GitBook, Docusaurus, etc. - -## Legend - -- 🟢 **Green**: Success/approval states -- 🟡 **Yellow**: Processing/agent executors -- 🟣 **Purple**: Review/evaluation -- 🔵 **Blue**: User/external -- 🔴 **Red**: Error states -- ➡️ **Solid arrows**: Direct message flow -- ⤏ **Dashed arrows**: Tool calls/side effects diff --git a/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md b/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md deleted file mode 100644 index fe91765f2..000000000 --- a/agentic_ai/agents/agent_framework/multi_agent/WORKFLOW_REFLECTION_README.md +++ /dev/null @@ -1,345 +0,0 @@ -# Workflow-Based Reflection Agent - -A workflow implementation of the reflection pattern using Agent Framework's `WorkflowBuilder`, featuring a 3-party communication design with quality assurance gates. - -## Overview - -This agent implements a sophisticated reflection pattern where responses are iteratively refined until they meet quality standards. Unlike the traditional two-agent reflection pattern, this uses a workflow-based approach with explicit conditional routing. - -## Architecture - -### 3-Party Communication Pattern - -``` -User → PrimaryAgent → ReviewerAgent → {approve: User, reject: PrimaryAgent} - ↑ | - |__________________________________________| - (feedback loop) -``` - -**Key Design Principles:** - -1. **PrimaryAgent**: Customer support agent that: - - Receives user messages with conversation history - - Cannot send messages directly to user - - All outputs go to ReviewerAgent for evaluation - - Uses MCP tools for data retrieval - -2. **ReviewerAgent**: Quality assurance gate that: - - Evaluates PrimaryAgent responses - - Acts as conditional router: - - `approve=true` → Emit to user - - `approve=false` → Send feedback to PrimaryAgent - - Has access to full conversation context - -3. **Conversation History**: - - Maintained between User and PrimaryAgent only - - Both agents receive history for context - - Updated only when approved responses are delivered - -## Features - -✅ **Workflow-Based Architecture** -- Built using `WorkflowBuilder` for explicit control flow -- Bidirectional edges between PrimaryAgent and ReviewerAgent -- Conditional routing based on structured review decisions - -✅ **Quality Assurance** -- Every response is reviewed before reaching the user -- Structured evaluation criteria: - - Accuracy of information - - Completeness of answer - - Professional tone - - Proper tool usage - - Clarity and helpfulness - -✅ **Iterative Refinement** -- Failed reviews trigger regeneration with feedback -- Conversation context preserved across iterations -- Unlimited refinement cycles until approval - -✅ **MCP Tool Integration** -- Supports MCP tools for external data access -- Tools available to both agents -- Proper authentication via bearer tokens - -✅ **Streaming Support** -- WebSocket-based streaming for real-time updates -- Progress indicators for each workflow stage -- Token-level streaming for agent responses - -## Implementation Details - -### Executor Classes - -#### `PrimaryAgentExecutor` -```python -class PrimaryAgentExecutor(Executor): - """ - Generates customer support responses. - Sends all outputs to ReviewerAgent. - """ - - @handler - async def handle_user_request( - self, request: PrimaryAgentRequest, ctx: WorkflowContext[ReviewRequest] - ) -> None: - # Generate response with conversation history - # Send to ReviewerAgent for evaluation - - @handler - async def handle_review_feedback( - self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest] - ) -> None: - # If not approved: incorporate feedback and regenerate - # Send refined response back to ReviewerAgent -``` - -#### `ReviewerAgentExecutor` -```python -class ReviewerAgentExecutor(Executor): - """ - Evaluates responses and acts as conditional gate. - """ - - @handler - async def review_response( - self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse] - ) -> None: - # Evaluate response quality - # If approved: emit to user via AgentRunUpdateEvent - # If not: send feedback to PrimaryAgent -``` - -### Message Flow - -1. **User Input** - ```python - PrimaryAgentRequest( - request_id=uuid4(), - user_prompt="What is customer 1's billing status?", - conversation_history=[...previous messages...] - ) - ``` - -2. **Primary Agent → Reviewer** - ```python - ReviewRequest( - request_id=request_id, - user_prompt="What is customer 1's billing status?", - conversation_history=[...], - primary_agent_response=[...ChatMessage...] - ) - ``` - -3. **Reviewer Decision** - ```python - ReviewDecision( - approved=True/False, - feedback="Constructive feedback or approval note" - ) - ``` - -4. **Conditional Routing** - - **Approved**: `AgentRunUpdateEvent` → User - - **Rejected**: `ReviewResponse` → PrimaryAgent → Loop back to step 2 - -### Workflow Graph - -```python -workflow = ( - WorkflowBuilder() - .add_edge(primary_agent, reviewer_agent) # Forward path - .add_edge(reviewer_agent, primary_agent) # Feedback path - .set_start_executor(primary_agent) - .build() - .as_agent() # Expose as standard agent interface -) -``` - -## Usage - -### Basic Usage - -```python -from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - -# Create agent instance -state_store = {} -session_id = "user_session_123" -agent = Agent(state_store=state_store, session_id=session_id) - -# Process user query -response = await agent.chat_async("Can you help me with customer ID 1?") -print(response) -``` - -### With Streaming - -```python -# Set WebSocket manager for streaming updates -agent.set_websocket_manager(ws_manager) - -# Chat will now stream progress updates -response = await agent.chat_async("What promotions are available?") -``` - -### With MCP Tools - -```python -# Set MCP_SERVER_URI environment variable -os.environ["MCP_SERVER_URI"] = "http://localhost:5000/mcp" - -# Agent will automatically use MCP tools -agent = Agent(state_store=state_store, session_id=session_id, access_token=token) -response = await agent.chat_async("Get billing summary for customer 1") -``` - -## Environment Variables - -Required: -- `AZURE_OPENAI_API_KEY`: Azure OpenAI API key -- `AZURE_OPENAI_CHAT_DEPLOYMENT`: Deployment name -- `AZURE_OPENAI_ENDPOINT`: Azure OpenAI endpoint URL -- `AZURE_OPENAI_API_VERSION`: API version (e.g., "2024-02-15-preview") -- `OPENAI_MODEL_NAME`: Model name (e.g., "gpt-4") - -Optional: -- `MCP_SERVER_URI`: URI for MCP server (enables tool usage) - -## Testing - -Run the test script: - -```bash -# From project root -python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py -``` - -The test script will: -1. Verify environment configuration -2. Run basic queries -3. Test MCP tool integration (if configured) -4. Display conversation history - -## Comparison: Workflow vs Traditional - -### Traditional Reflection Agent (`reflection_agent.py`) -- Direct agent-to-agent communication via `run()` calls -- Sequential execution (Step 1 → Step 2 → Step 3) -- Implicit control flow -- Manual state management - -### Workflow Reflection Agent (`reflection_workflow_agent.py`) -- Message-based communication via `WorkflowContext` -- Graph-based execution (workflow edges) -- Explicit conditional routing -- Framework-managed state -- Better scalability for complex workflows - -## Advanced Features - -### Custom Review Criteria - -Modify the ReviewerAgent's system prompt to enforce custom quality standards: - -```python -# In ReviewerAgentExecutor.__init__ -custom_criteria = """ -Review for: -1. Response time < 2 seconds -2. Includes specific customer name -3. References at least 2 data points -4. Professional greeting and closing -""" -``` - -### Multiple Refinement Rounds Limit - -Add a counter to prevent infinite loops: - -```python -class PrimaryAgentExecutor(Executor): - def __init__(self, max_refinements: int = 3): - self._max_refinements = max_refinements - self._refinement_counts = {} - - async def handle_review_feedback(self, review, ctx): - count = self._refinement_counts.get(review.request_id, 0) - if count >= self._max_refinements: - # Force approval or escalate - return -``` - -### Logging and Monitoring - -All workflow events are logged with structured information: - -```python -logger.info(f"[PrimaryAgent] Processing request {request_id[:8]}") -logger.info(f"[ReviewerAgent] Review decision - Approved: {approved}") -``` - -Enable debug logging for detailed traces: - -```python -logging.basicConfig(level=logging.DEBUG) -``` - -## Best Practices - -1. **Conversation History Management** - - Keep history concise (last N messages) - - Summarize old conversations for long sessions - -2. **Error Handling** - - Handle MCP tool failures gracefully - - Implement retry logic with exponential backoff - -3. **Performance** - - Use streaming for better user experience - - Consider caching for frequent queries - -4. **Security** - - Always validate MCP tool responses - - Sanitize user inputs - - Use bearer tokens for authentication - -## Troubleshooting - -### Common Issues - -**Issue**: Agent not using MCP tools -- **Solution**: Verify `MCP_SERVER_URI` is set and server is running - -**Issue**: Infinite refinement loop -- **Solution**: Check ReviewerAgent criteria are achievable, add max refinement limit - -**Issue**: Missing conversation context -- **Solution**: Ensure history is properly loaded from state_store - -**Issue**: Workflow hangs -- **Solution**: Check for unhandled message types, verify all edges are configured - -## Future Enhancements - -- [ ] Support for multi-modal inputs (images, files) -- [ ] Parallel reviewer agents (consensus-based approval) -- [ ] A/B testing of different review criteria -- [ ] Metrics and analytics dashboard -- [ ] Human-in-the-loop escalation for uncertain cases -- [ ] Fine-tuned reviewer models - -## Related Examples - -- `reference/agent-framework/python/samples/getting_started/workflows/agents/workflow_as_agent_reflection_pattern_azure.py` - Two-agent reflection -- `reference/agent-framework/python/samples/getting_started/workflows/agents/workflow_as_agent_human_in_the_loop_azure.py` - Human escalation -- `reference/agent-framework/python/samples/getting_started/workflows/control-flow/edge_condition.py` - Conditional routing - -## License - -This code is part of the OpenAI Workshop project. See LICENSE file for details. - -## Contributing - -Contributions are welcome! Please follow the project's contribution guidelines. diff --git a/agentic_ai/agents/agent_framework/multi_agent/handoff_multi_domain_agent.py b/agentic_ai/agents/agent_framework/multi_agent/handoff_multi_domain_agent.py index 059a3ac6f..dca76b14a 100644 --- a/agentic_ai/agents/agent_framework/multi_agent/handoff_multi_domain_agent.py +++ b/agentic_ai/agents/agent_framework/multi_agent/handoff_multi_domain_agent.py @@ -25,7 +25,7 @@ from agent_framework import ChatAgent, ChatMessage, Role, MCPStreamableHTTPTool from agent_framework.azure import AzureOpenAIChatClient -from agents.base_agent import BaseAgent +from agents.base_agent import BaseAgent, ToolCallTrackingMixin from agents.agent_framework.utils import create_filtered_tool_list logger = logging.getLogger(__name__) @@ -158,7 +158,7 @@ class IntentClassification(BaseModel): """ -class Agent(BaseAgent): +class Agent(ToolCallTrackingMixin, BaseAgent): """ Optimized handoff pattern using vanilla workflow and direct agent communication. @@ -184,6 +184,9 @@ def __init__(self, state_store: Dict[str, Any], session_id: str, access_token: s self._turn_key = f"{session_id}_handoff_turn" self._current_turn = state_store.get(self._turn_key, 0) + # Initialize tool tracking from mixin + self.init_tool_tracking() + # Context transfer configuration: -1 = all history, 0 = none, N = last N turns self._context_transfer_turns = int(os.getenv("HANDOFF_CONTEXT_TRANSFER_TURNS", "-1")) @@ -510,6 +513,9 @@ async def chat_async(self, prompt: str) -> str: """ await self._setup_agents() + # Clear tool calls from previous request (from mixin) + self.clear_tool_calls() + # Increment turn counter self._current_turn += 1 self.state_store[self._turn_key] = self._current_turn @@ -589,18 +595,31 @@ async def chat_async(self, prompt: str) -> str: # Process contents in the chunk if hasattr(chunk, 'contents') and chunk.contents: for content in chunk.contents: - # Check for tool/function calls + # Check for tool/function calls - track with arguments if content.type == "function_call": - if self._ws_manager: - await self._ws_manager.broadcast( - self.session_id, - { - "type": "tool_called", - "agent_id": target_domain, - "tool_name": content.name, - "turn": self._current_turn, - }, - ) + if content.name: + # New function call - finalize previous and start new + self.track_function_call_start(content.name) + + if self._ws_manager: + await self._ws_manager.broadcast( + self.session_id, + { + "type": "tool_called", + "agent_id": target_domain, + "tool_name": content.name, + "turn": self._current_turn, + }, + ) + + # Accumulate arguments + args_chunk = getattr(content, 'arguments', '') + if args_chunk: + self.track_function_call_arguments(args_chunk) + + elif content.type == "function_result": + # Function completed - finalize + self.finalize_tool_tracking() # Extract text from chunk if hasattr(chunk, 'text') and chunk.text: @@ -620,6 +639,9 @@ async def chat_async(self, prompt: str) -> str: except Exception as exc: logger.error(f"[HANDOFF] Error during agent streaming: {exc}", exc_info=True) raise + + # Finalize any remaining function call + self.finalize_tool_tracking() assistant_response = ''.join(full_response) @@ -682,16 +704,26 @@ async def chat_async(self, prompt: str) -> str: if hasattr(chunk, 'contents') and chunk.contents: for content in chunk.contents: if content.type == "function_call": - if self._ws_manager: - await self._ws_manager.broadcast( - self.session_id, - { - "type": "tool_called", - "agent_id": new_target_domain, - "tool_name": content.name, - "turn": self._current_turn, - }, - ) + if content.name: + self.track_function_call_start(content.name) + + if self._ws_manager: + await self._ws_manager.broadcast( + self.session_id, + { + "type": "tool_called", + "agent_id": new_target_domain, + "tool_name": content.name, + "turn": self._current_turn, + }, + ) + + args_chunk = getattr(content, 'arguments', '') + if args_chunk: + self.track_function_call_arguments(args_chunk) + + elif content.type == "function_result": + self.finalize_tool_tracking() if hasattr(chunk, 'text') and chunk.text: full_response_handoff.append(chunk.text) @@ -709,6 +741,9 @@ async def chat_async(self, prompt: str) -> str: logger.error(f"[HANDOFF] Error during handoff agent streaming: {exc}", exc_info=True) raise + # Finalize any remaining function call + self.finalize_tool_tracking() + # Use handoff response assistant_response = ''.join(full_response_handoff) target_domain = new_target_domain diff --git a/agentic_ai/agents/agent_framework/multi_agent/magentic_group.py b/agentic_ai/agents/agent_framework/multi_agent/magentic_group.py index 7460a1eb1..7054bcf2e 100644 --- a/agentic_ai/agents/agent_framework/multi_agent/magentic_group.py +++ b/agentic_ai/agents/agent_framework/multi_agent/magentic_group.py @@ -20,7 +20,7 @@ ) from agent_framework.azure import AzureOpenAIChatClient # type: ignore[import] -from agents.base_agent import BaseAgent +from agents.base_agent import BaseAgent, ToolCallTrackingMixin from agents.agent_framework.utils import create_filtered_tool_list logger = logging.getLogger(__name__) @@ -104,7 +104,7 @@ def clear_all(self) -> None: self._backing.pop("pending_prompt", None) -class Agent(BaseAgent): +class Agent(ToolCallTrackingMixin, BaseAgent): """Agent Framework implementation of the collaborative Magentic team.""" DEFAULT_MANAGER_INSTRUCTIONS = ( @@ -226,6 +226,9 @@ def __init__( self._stream_agent_id: Optional[str] = None self._stream_line_open: bool = False self._last_agent_message: Optional[str] = None # Track last agent message for deduplication + + # Initialize tool tracking from mixin + self.init_tool_tracking() def set_websocket_manager(self, manager: Any) -> None: """Allow backend to inject WebSocket manager for streaming events.""" diff --git a/agentic_ai/agents/agent_framework/multi_agent/reflection_agent.py b/agentic_ai/agents/agent_framework/multi_agent/reflection_agent.py index 5a61e43e7..e06b020c8 100644 --- a/agentic_ai/agents/agent_framework/multi_agent/reflection_agent.py +++ b/agentic_ai/agents/agent_framework/multi_agent/reflection_agent.py @@ -13,7 +13,7 @@ from agent_framework import AgentThread, ChatAgent, MCPStreamableHTTPTool from agent_framework.azure import AzureOpenAIChatClient -from agents.base_agent import BaseAgent +from agents.base_agent import BaseAgent, ToolCallTrackingMixin logger = logging.getLogger(__name__) @@ -37,7 +37,7 @@ } -class Agent(BaseAgent): +class Agent(ToolCallTrackingMixin, BaseAgent): """Reflection Agent with Primary Agent + Reviewer workflow.""" def __init__( @@ -55,6 +55,8 @@ def __init__( self._access_token = access_token self._ws_manager = None self._max_refinements = max_refinements + # Initialize tool tracking from mixin + self.init_tool_tracking() logger.info(f"[Reflection] Initialized session: {session_id}") def set_websocket_manager(self, manager: Any) -> None: @@ -154,12 +156,48 @@ async def _run_agent( prompt: str, agent_id: str, ) -> str: - """Run an agent with optional streaming.""" + """Run an agent with optional streaming. + + Even without WebSocket, we use run_stream to capture tool calls for evaluation. + """ if self._ws_manager: return await self._run_agent_streaming(agent, prompt, agent_id) else: - result = await agent.run(prompt, thread=self._thread) - return result.text + # Use run_stream even without WebSocket to capture tool calls + return await self._run_agent_non_streaming(agent, prompt, agent_id) + + async def _run_agent_non_streaming( + self, + agent: ChatAgent, + prompt: str, + agent_id: str, + ) -> str: + """Run agent without WebSocket but still capture tool calls.""" + chunks: List[str] = [] + + async for chunk in agent.run_stream(prompt, thread=self._thread): + # Track tool calls for evaluation + if hasattr(chunk, 'contents') and chunk.contents: + for content in chunk.contents: + if content.type == "function_call": + if content.name: + self.track_function_call_start(content.name) + + args_chunk = getattr(content, 'arguments', '') + if args_chunk: + self.track_function_call_arguments(args_chunk) + + elif content.type == "function_result": + self.finalize_tool_tracking() + + # Collect text + if hasattr(chunk, 'text') and chunk.text: + chunks.append(chunk.text) + + # Finalize any remaining function call + self.finalize_tool_tracking() + + return ''.join(chunks) async def _run_agent_streaming( self, @@ -179,15 +217,25 @@ async def _run_agent_streaming( chunks: List[str] = [] async for chunk in agent.run_stream(prompt, thread=self._thread): - # Handle tool calls + # Handle tool calls with argument tracking if hasattr(chunk, 'contents') and chunk.contents: for content in chunk.contents: if content.type == "function_call": - await self._broadcast_raw({ - "type": "tool_called", - "agent_id": agent_id, - "tool_name": content.name, - }) + if content.name: + self.track_function_call_start(content.name) + + await self._broadcast_raw({ + "type": "tool_called", + "agent_id": agent_id, + "tool_name": content.name, + }) + + args_chunk = getattr(content, 'arguments', '') + if args_chunk: + self.track_function_call_arguments(args_chunk) + + elif content.type == "function_result": + self.finalize_tool_tracking() # Stream text if hasattr(chunk, 'text') and chunk.text: @@ -198,6 +246,9 @@ async def _run_agent_streaming( "content": chunk.text, }) + # Finalize any remaining function call + self.finalize_tool_tracking() + response = ''.join(chunks) # Send complete message @@ -222,6 +273,9 @@ async def chat_async(self, prompt: str) -> str: if not self._primary_agent or not self._reviewer or not self._thread: raise RuntimeError("Agents not initialized") + # Clear tool calls from previous request (from mixin) + self.clear_tool_calls() + # Notify start await self._broadcast("plan", "🔄 Reflection Workflow\n\nStarting Primary Agent → Reviewer pipeline...") diff --git a/agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py b/agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py deleted file mode 100644 index c23d3882e..000000000 --- a/agentic_ai/agents/agent_framework/multi_agent/reflection_workflow_agent.py +++ /dev/null @@ -1,645 +0,0 @@ -""" -Agent Framework Workflow-based Reflection Agent - -This implementation uses the WorkflowBuilder pattern with a 3-party communication flow: -User -> PrimaryAgent -> ReviewerAgent -> User (if approved) OR back to PrimaryAgent (if rejected) - -Key Design: -- PrimaryAgent receives user messages but cannot send directly to user -- All PrimaryAgent outputs go to ReviewerAgent for evaluation -- ReviewerAgent acts as a conditional gate: approve or request_for_edit -- Conversation history is maintained between user and PrimaryAgent only -- History is passed to both agents for context -""" - -import json -import logging -from dataclasses import dataclass -from typing import Any, Dict, List -from uuid import uuid4 - -from agent_framework import ( - AgentRunResponseUpdate, - AgentRunUpdateEvent, - ChatMessage, - Contents, - Executor, - MCPStreamableHTTPTool, - Role, - WorkflowBuilder, - WorkflowContext, - handler, -) -from agent_framework.azure import AzureOpenAIChatClient -from pydantic import BaseModel - -from agents.base_agent import BaseAgent - -logger = logging.getLogger(__name__) - - -class ReviewDecision(BaseModel): - """Structured output from ReviewerAgent for reliable routing.""" - approved: bool - feedback: str - - -@dataclass -class PrimaryAgentRequest: - """Request sent to PrimaryAgent with conversation history.""" - request_id: str - user_prompt: str - conversation_history: list[ChatMessage] - - -@dataclass -class ReviewRequest: - """Request sent from PrimaryAgent to ReviewerAgent.""" - request_id: str - user_prompt: str - conversation_history: list[ChatMessage] - primary_agent_response: list[ChatMessage] - - -@dataclass -class ReviewResponse: - """Response from ReviewerAgent back to PrimaryAgent.""" - request_id: str - approved: bool - feedback: str - - -class PrimaryAgentExecutor(Executor): - """ - Primary Agent - Customer Support Agent with MCP tools. - Receives user messages and generates responses sent to ReviewerAgent for approval. - """ - - def __init__( - self, - id: str, - chat_client: AzureOpenAIChatClient, - tools: MCPStreamableHTTPTool | None = None, - model: str | None = None, - max_refinements: int = 3, - ) -> None: - super().__init__(id=id) - self._chat_client = chat_client - self._tools = tools - self._model = model - self._max_refinements = max_refinements - # Track pending requests for retry with feedback - self._pending_requests: dict[str, tuple[PrimaryAgentRequest, list[ChatMessage]]] = {} - # Track refinement counts to prevent infinite loops - self._refinement_counts: dict[str, int] = {} - - @handler - async def handle_user_request( - self, request: PrimaryAgentRequest, ctx: WorkflowContext[ReviewRequest] - ) -> None: - """Handle initial user request with conversation history.""" - print(f"[PrimaryAgent] Processing user request (ID: {request.request_id[:8]})") - logger.info(f"[PrimaryAgent] Processing user request (ID: {request.request_id[:8]})") - - # Build message list with system prompt, history, and new user message - messages = [ - ChatMessage( - role=Role.SYSTEM, - text=( - "You are a helpful customer support assistant for Contoso company. " - "You can help with billing, promotions, security, account information, and other customer inquiries. " - "Use the available MCP tools to look up customer information, billing details, promotions, and security settings. " - "When a customer provides an ID or asks about their account, use the tools to retrieve accurate, up-to-date information. " - "Always be helpful, professional, and provide detailed information when available." - ), - ) - ] - - # Add conversation history for context - messages.extend(request.conversation_history) - - # Add current user prompt - messages.append(ChatMessage(role=Role.USER, text=request.user_prompt)) - - print(f"[PrimaryAgent] Generating response with {len(messages)} messages in context") - logger.info(f"[PrimaryAgent] Generating response with {len(messages)} messages in context") - - # Generate response - response = await self._chat_client.get_response( - messages=messages, - tools=self._tools, - model=self._model, - ) - - print(f"[PrimaryAgent] Response generated: {response.messages[-1].text[:100]}...") - logger.info(f"[PrimaryAgent] Response generated") - - # Store full message context for potential retry - all_messages = messages + response.messages - self._pending_requests[request.request_id] = (request, all_messages) - - # Initialize refinement counter - if request.request_id not in self._refinement_counts: - self._refinement_counts[request.request_id] = 0 - - # Send to ReviewerAgent for evaluation - review_request = ReviewRequest( - request_id=request.request_id, - user_prompt=request.user_prompt, - conversation_history=request.conversation_history, - primary_agent_response=response.messages, - ) - - print(f"[PrimaryAgent] Sending response to ReviewerAgent for evaluation") - logger.info(f"[PrimaryAgent] Sending response to ReviewerAgent for evaluation") - await ctx.send_message(review_request) - - @handler - async def handle_review_feedback( - self, review: ReviewResponse, ctx: WorkflowContext[ReviewRequest] - ) -> None: - """Handle feedback from ReviewerAgent and regenerate if needed.""" - print(f"[PrimaryAgent] Received review (ID: {review.request_id[:8]}) - Approved: {review.approved}") - logger.info(f"[PrimaryAgent] Received review (ID: {review.request_id[:8]}) - Approved: {review.approved}") - - if review.request_id not in self._pending_requests: - logger.error(f"[PrimaryAgent] Unknown request ID: {review.request_id}") - raise ValueError(f"Unknown request ID in review: {review.request_id}") - - original_request, messages = self._pending_requests.pop(review.request_id) - - if review.approved: - print(f"[PrimaryAgent] Response approved! Sending to user via WorkflowAgent") - logger.info(f"[PrimaryAgent] Response approved") - - # Clean up refinement counter - self._refinement_counts.pop(review.request_id, None) - - # Extract contents from response to emit to user - # The WorkflowAgent will handle emitting this to the external consumer - # We don't send directly - ReviewerAgent will handle final emission - return - - # Check if we've exceeded max refinements - current_count = self._refinement_counts.get(review.request_id, 0) - if current_count >= self._max_refinements: - print(f"[PrimaryAgent] Max refinements ({self._max_refinements}) reached. Force approving response.") - logger.warning(f"[PrimaryAgent] Max refinements reached for request {review.request_id[:8]}") - - # Clean up - self._refinement_counts.pop(review.request_id, None) - - # Force emit the last response even though not approved - # The ReviewerAgent already sent the ReviewResponse, so we're done - return - - # Increment refinement counter - self._refinement_counts[review.request_id] = current_count + 1 - - # Not approved - incorporate feedback and regenerate - print(f"[PrimaryAgent] Response not approved (attempt {current_count + 1}/{self._max_refinements}). Feedback: {review.feedback[:100]}...") - logger.info(f"[PrimaryAgent] Regenerating with feedback (attempt {current_count + 1}/{self._max_refinements})") - - # Add feedback to message context - messages.append( - ChatMessage( - role=Role.SYSTEM, - text=f"REVIEWER FEEDBACK: {review.feedback}\n\nPlease improve your response based on this feedback.", - ) - ) - - # Add the original user prompt again for clarity - messages.append(ChatMessage(role=Role.USER, text=original_request.user_prompt)) - - # Regenerate response - response = await self._chat_client.get_response( - messages=messages, - tools=self._tools, - model=self._model, - ) - - print(f"[PrimaryAgent] New response generated: {response.messages[-1].text[:100]}...") - logger.info(f"[PrimaryAgent] New response generated") - - # Update stored messages - messages.extend(response.messages) - self._pending_requests[review.request_id] = (original_request, messages) - - # Send updated response for re-review - review_request = ReviewRequest( - request_id=review.request_id, - user_prompt=original_request.user_prompt, - conversation_history=original_request.conversation_history, - primary_agent_response=response.messages, - ) - - print(f"[PrimaryAgent] Sending refined response to ReviewerAgent") - logger.info(f"[PrimaryAgent] Sending refined response to ReviewerAgent") - await ctx.send_message(review_request) - - -class ReviewerAgentExecutor(Executor): - """ - Reviewer Agent - Quality assurance gate. - Evaluates PrimaryAgent responses for accuracy, completeness, and professionalism. - Acts as conditional gate: approved responses go to user, rejected go back to PrimaryAgent. - """ - - def __init__( - self, - id: str, - chat_client: AzureOpenAIChatClient, - tools: MCPStreamableHTTPTool | None = None, - model: str | None = None, - ) -> None: - super().__init__(id=id) - self._chat_client = chat_client - self._tools = tools - self._model = model - - @handler - async def review_response( - self, request: ReviewRequest, ctx: WorkflowContext[ReviewResponse] - ) -> None: - """ - Review the PrimaryAgent's response and decide: approve or request edit. - Approved responses are emitted to user via AgentRunUpdateEvent. - Rejected responses are sent back to PrimaryAgent with feedback. - """ - print(f"[ReviewerAgent] Evaluating response (ID: {request.request_id[:8]})") - logger.info(f"[ReviewerAgent] Evaluating response (ID: {request.request_id[:8]})") - - # Build review context with conversation history - messages = [ - ChatMessage( - role=Role.SYSTEM, - text=( - "You are a quality assurance reviewer for customer support responses. " - "Review the customer support agent's response for:\n" - "1. Accuracy of information\n" - "2. Completeness of answer\n" - "3. Professional tone\n" - "4. Proper use of available tools\n" - "5. Clarity and helpfulness\n\n" - "Be reasonable in your evaluation. If the response is professional, addresses the customer's question, " - "and provides useful information, APPROVE it. Only reject if there are significant issues.\n\n" - "Respond with a structured JSON containing:\n" - "- approved: true if response meets quality standards (be reasonable), false only for major issues\n" - "- feedback: constructive feedback (if not approved) or brief approval note" - ), - ) - ] - - # Add conversation history for context - messages.extend(request.conversation_history) - - # Add the user's question - messages.append(ChatMessage(role=Role.USER, text=request.user_prompt)) - - # Add the agent's response - messages.extend(request.primary_agent_response) - - # Add explicit review instruction - messages.append( - ChatMessage( - role=Role.USER, - text="Please review the agent's response above and provide your assessment.", - ) - ) - - print(f"[ReviewerAgent] Sending review request to LLM") - logger.info(f"[ReviewerAgent] Sending review request to LLM") - - # Get structured review decision - response = await self._chat_client.get_response( - messages=messages, - response_format=ReviewDecision, - tools=self._tools, - model=self._model, - ) - - # Parse decision - decision = ReviewDecision.model_validate_json(response.messages[-1].text) - - print(f"[ReviewerAgent] Review decision - Approved: {decision.approved}") - if not decision.approved: - print(f"[ReviewerAgent] Feedback: {decision.feedback[:100]}...") - logger.info(f"[ReviewerAgent] Review decision - Approved: {decision.approved}") - - if decision.approved: - # Emit approved response to external consumer (user) - print(f"[ReviewerAgent] Emitting approved response to user") - logger.info(f"[ReviewerAgent] Emitting approved response to user") - - contents: list[Contents] = [] - for message in request.primary_agent_response: - contents.extend(message.contents) - - await ctx.add_event( - AgentRunUpdateEvent(self.id, data=AgentRunResponseUpdate(contents=contents, role=Role.ASSISTANT)) - ) - else: - # Send feedback back to PrimaryAgent for refinement - print(f"[ReviewerAgent] Sending feedback to PrimaryAgent for refinement") - logger.info(f"[ReviewerAgent] Sending feedback to PrimaryAgent for refinement") - - # Always send review response back to enable loop continuation - await ctx.send_message( - ReviewResponse( - request_id=request.request_id, - approved=decision.approved, - feedback=decision.feedback, - ) - ) - - -class Agent(BaseAgent): - """ - Workflow-based Reflection Agent implementation. - - Implements a 3-party communication pattern: - User -> PrimaryAgent -> ReviewerAgent -> User (if approved) OR back to PrimaryAgent (if not) - - Conversation history is maintained between user and PrimaryAgent only. - Both agents receive history for context. - """ - - def __init__(self, state_store: Dict[str, Any], session_id: str, access_token: str | None = None) -> None: - super().__init__(state_store, session_id) - self._workflow = None - self._initialized = False - self._access_token = access_token - self._ws_manager = None - self._mcp_tool = None # Store connected MCP tool - - # Track conversation history as ChatMessage objects - self._conversation_history: list[ChatMessage] = [] - self._load_conversation_history() - - print(f"WORKFLOW REFLECTION AGENT INITIALIZED - Session: {session_id}") - logger.info(f"WORKFLOW REFLECTION AGENT INITIALIZED - Session: {session_id}") - - def _load_conversation_history(self) -> None: - """Load conversation history from state store and convert to ChatMessage format.""" - chat_history = self.chat_history # From BaseAgent - for msg in chat_history: - role = Role.USER if msg.get("role") == "user" else Role.ASSISTANT - text = msg.get("content", "") - self._conversation_history.append(ChatMessage(role=role, text=text)) - - logger.info(f"Loaded {len(self._conversation_history)} messages from history") - - def set_websocket_manager(self, manager: Any) -> None: - """Allow backend to inject WebSocket manager for streaming events.""" - self._ws_manager = manager - logger.info(f"[STREAMING] WebSocket manager set for workflow reflection agent, session_id={self.session_id}") - - async def _setup_workflow(self) -> None: - """Initialize the workflow with PrimaryAgent and ReviewerAgent executors.""" - if self._initialized: - return - - if not all([self.azure_openai_key, self.azure_deployment, self.azure_openai_endpoint, self.api_version]): - raise RuntimeError( - "Azure OpenAI configuration is incomplete. Ensure AZURE_OPENAI_API_KEY, " - "AZURE_OPENAI_CHAT_DEPLOYMENT, AZURE_OPENAI_ENDPOINT, and AZURE_OPENAI_API_VERSION are set." - ) - - print(f"[WORKFLOW] Setting up workflow agents...") - logger.info(f"[WORKFLOW] Setting up workflow agents") - - # Setup MCP tools if configured (create only once) - if not self._mcp_tool: - headers = self._build_headers() - mcp_tools = await self._maybe_create_tools(headers) - self._mcp_tool = mcp_tools[0] if mcp_tools else None - - if self._mcp_tool: - print(f"[WORKFLOW] MCP tool created (will connect on first use)") - logger.info(f"[WORKFLOW] MCP tool created") - - # Create Azure OpenAI chat client - chat_client = AzureOpenAIChatClient( - api_key=self.azure_openai_key, - deployment_name=self.azure_deployment, - endpoint=self.azure_openai_endpoint, - api_version=self.api_version, - ) - - # Create executors - primary_agent = PrimaryAgentExecutor( - id="primary_agent", - chat_client=chat_client, - tools=self._mcp_tool, - model=self.openai_model_name, - ) - - reviewer_agent = ReviewerAgentExecutor( - id="reviewer_agent", - chat_client=chat_client, - tools=self._mcp_tool, - model=self.openai_model_name, - ) - - print(f"[WORKFLOW] Building workflow graph: PrimaryAgent <-> ReviewerAgent") - logger.info(f"[WORKFLOW] Building workflow graph") - - # Build workflow with bidirectional edges - self._workflow = ( - WorkflowBuilder() - .add_edge(primary_agent, reviewer_agent) # Primary -> Reviewer - .add_edge(reviewer_agent, primary_agent) # Reviewer -> Primary (for feedback) - .set_start_executor(primary_agent) - .build() - ) - - self._initialized = True - print(f"[WORKFLOW] Workflow initialization complete") - logger.info(f"[WORKFLOW] Workflow initialization complete") - - def _build_headers(self) -> Dict[str, str]: - """Build HTTP headers for MCP tool requests.""" - headers = {"Content-Type": "application/json"} - if self._access_token: - headers["Authorization"] = f"Bearer {self._access_token}" - return headers - - async def _maybe_create_tools(self, headers: Dict[str, str]) -> List[MCPStreamableHTTPTool] | None: - """Create MCP tools if server URI is configured.""" - if not self.mcp_server_uri: - logger.warning("MCP_SERVER_URI not configured; agents run without MCP tools.") - return None - - print(f"[WORKFLOW] Creating MCP tools with server: {self.mcp_server_uri}") - return [ - MCPStreamableHTTPTool( - name="mcp-streamable", - url=self.mcp_server_uri, - headers=headers, - timeout=30, - request_timeout=30, - ) - ] - - async def chat_async(self, prompt: str) -> str: - """ - Process user prompt through the reflection workflow. - - Flow: - 1. Create PrimaryAgentRequest with conversation history - 2. PrimaryAgent generates response - 3. ReviewerAgent evaluates response - 4. If approved -> return to user - 5. If not approved -> PrimaryAgent refines with feedback (loop continues) - """ - print(f"WORKFLOW REFLECTION AGENT chat_async called with prompt: {prompt[:50]}...") - logger.info(f"WORKFLOW REFLECTION AGENT chat_async called with prompt: {prompt[:50]}...") - - await self._setup_workflow() - if not self._workflow: - raise RuntimeError("Workflow not initialized correctly.") - - # Create request with conversation history - request_id = str(uuid4()) - request = PrimaryAgentRequest( - request_id=request_id, - user_prompt=prompt, - conversation_history=self._conversation_history.copy(), - ) - - print(f"[WORKFLOW] Starting workflow execution (Request ID: {request_id[:8]})") - logger.info(f"[WORKFLOW] Starting workflow execution") - - # Run workflow (streaming or non-streaming based on ws_manager) - if self._ws_manager: - print(f"[WORKFLOW] Using STREAMING mode") - logger.info(f"[WORKFLOW] Using STREAMING mode") - response_text = await self._run_workflow_streaming(request) - else: - print(f"[WORKFLOW] Using NON-STREAMING mode") - logger.info(f"[WORKFLOW] Using NON-STREAMING mode") - response_text = await self._run_workflow(request) - - # Update conversation history - self._conversation_history.append(ChatMessage(role=Role.USER, text=prompt)) - self._conversation_history.append(ChatMessage(role=Role.ASSISTANT, text=response_text)) - - # Update chat history in base class format - messages = [ - {"role": "user", "content": prompt}, - {"role": "assistant", "content": response_text}, - ] - self.append_to_chat_history(messages) - - print(f"[WORKFLOW] Workflow execution complete") - logger.info(f"[WORKFLOW] Workflow execution complete") - - return response_text - - async def _run_workflow(self, request: PrimaryAgentRequest) -> str: - """Run workflow in non-streaming mode.""" - # Run the workflow directly with the custom request - response = await self._workflow.run(request) - - # Extract text from the workflow result - response_text = response.output if hasattr(response, 'output') else str(response) - - print(f"[WORKFLOW] Response received: {response_text[:100]}...") - logger.info(f"[WORKFLOW] Response received") - - return response_text - - async def _run_workflow_streaming(self, request: PrimaryAgentRequest) -> str: - """Run workflow in streaming mode with WebSocket updates.""" - - # Notify UI that workflow is starting - if self._ws_manager: - await self._ws_manager.broadcast( - self.session_id, - { - "type": "orchestrator", - "kind": "plan", - "content": "Workflow Reflection Pattern Starting\n\nInitiating PrimaryAgent → ReviewerAgent workflow for quality-assured responses...", - }, - ) - - response_text = "" - - try: - async for event in self._workflow.run_stream(request): - # Handle different event types - event_str = str(event) - print(f"[WORKFLOW STREAM] Event: {event_str[:100]}...") - - # Check if this is an AgentRunUpdateEvent with approved response - if isinstance(event, AgentRunUpdateEvent): - print(f"[WORKFLOW STREAM] AgentRunUpdateEvent detected from {event.executor_id}") - - # Extract response from the event data - if hasattr(event, 'data') and isinstance(event.data, AgentRunResponseUpdate): - # Extract text from contents - for content in event.data.contents: - if hasattr(content, 'text') and content.text: - response_text += content.text - - # Stream to WebSocket - if self._ws_manager: - await self._ws_manager.broadcast( - self.session_id, - { - "type": "agent_token", - "agent_id": "workflow_reflection", - "content": content.text, - }, - ) - - print(f"[WORKFLOW STREAM] Extracted response text: {response_text[:100]}...") - - # Also check for text attribute directly on event - elif hasattr(event, 'text') and event.text: - response_text += event.text - - # Stream to WebSocket - if self._ws_manager: - await self._ws_manager.broadcast( - self.session_id, - { - "type": "agent_token", - "agent_id": "workflow_reflection", - "content": event.text, - }, - ) - - # Check for messages attribute - elif hasattr(event, 'messages'): - for msg in event.messages: - if hasattr(msg, 'text') and msg.text: - response_text = msg.text - - # Send final result - if self._ws_manager and response_text: - await self._ws_manager.broadcast( - self.session_id, - { - "type": "final_result", - "content": response_text, - }, - ) - - await self._ws_manager.broadcast( - self.session_id, - { - "type": "orchestrator", - "kind": "result", - "content": "Workflow Complete\n\nQuality-assured response delivered through PrimaryAgent → ReviewerAgent workflow!", - }, - ) - - except Exception as exc: - logger.error(f"[WORKFLOW] Error during streaming: {exc}", exc_info=True) - raise - - print(f"[WORKFLOW STREAM] Complete. Response length: {len(response_text)}") - logger.info(f"[WORKFLOW STREAM] Complete") - - return response_text diff --git a/agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py b/agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py deleted file mode 100644 index 072bd8985..000000000 --- a/agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py +++ /dev/null @@ -1,226 +0,0 @@ -""" -Test script for the Workflow-based Reflection Agent - -This script demonstrates the 3-party communication pattern: -User -> PrimaryAgent -> ReviewerAgent -> User (if approved) OR back to PrimaryAgent (if not) - -Usage: - python test_reflection_workflow_agent.py -""" - -import asyncio -import logging -import os -from typing import Dict, Any - -# Setup logging -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) - -logger = logging.getLogger(__name__) - - -async def test_workflow_reflection_agent(): - """Test the workflow-based reflection agent.""" - - print("=" * 70) - print("WORKFLOW REFLECTION AGENT TEST") - print("=" * 70) - print() - - # Check environment variables - required_env_vars = [ - "AZURE_OPENAI_API_KEY", - "AZURE_OPENAI_CHAT_DEPLOYMENT", - "AZURE_OPENAI_ENDPOINT", - "AZURE_OPENAI_API_VERSION", - "OPENAI_MODEL_NAME", - ] - - print("Checking environment variables...") - missing_vars = [var for var in required_env_vars if not os.getenv(var)] - if missing_vars: - print(f"❌ Missing environment variables: {', '.join(missing_vars)}") - print("\nPlease set the following environment variables:") - for var in missing_vars: - print(f" - {var}") - return - - print("✓ All required environment variables are set") - print() - - # Optional MCP server - mcp_uri = os.getenv("MCP_SERVER_URI") - if mcp_uri: - print(f"✓ MCP Server configured: {mcp_uri}") - else: - print("ℹ MCP Server not configured (agents will work without MCP tools)") - print() - - # Import the agent (after env check to avoid import errors) - try: - from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - except ImportError as e: - print(f"❌ Failed to import Agent: {e}") - print("\nMake sure you're running from the project root directory:") - print(" python agentic_ai/agents/agent_framework/multi_agent/test_reflection_workflow_agent.py") - return - - # Create state store and agent - state_store: Dict[str, Any] = {} - session_id = "test_session_001" - - print(f"Creating Workflow Reflection Agent (Session: {session_id})...") - agent = Agent(state_store=state_store, session_id=session_id) - print("✓ Agent created successfully") - print() - - # Test queries - test_queries = [ - "What is the capital of France?", - "Can you help me with customer ID 1?", - ] - - for i, query in enumerate(test_queries, 1): - print("=" * 70) - print(f"TEST QUERY {i}: {query}") - print("=" * 70) - print() - - try: - print(f"Sending query to agent...") - print(f"Expected flow: User -> PrimaryAgent -> ReviewerAgent -> (approve/reject)") - print() - - response = await agent.chat_async(query) - - print() - print("-" * 70) - print("FINAL RESPONSE:") - print("-" * 70) - print(response) - print() - - print("✓ Query completed successfully") - print() - - except Exception as e: - print(f"❌ Error during query: {e}") - logger.error(f"Error during query: {e}", exc_info=True) - print() - - print("=" * 70) - print("TEST COMPLETE") - print("=" * 70) - print() - print("Summary:") - print(f"- Total queries tested: {len(test_queries)}") - print(f"- Session ID: {session_id}") - print(f"- Conversation history entries: {len(state_store.get(f'{session_id}_chat_history', []))}") - print() - print("Key features demonstrated:") - print(" ✓ 3-party communication pattern (User -> PrimaryAgent -> ReviewerAgent)") - print(" ✓ Conditional gate (approve/reject)") - print(" ✓ Conversation history maintenance") - print(" ✓ Iterative refinement loop") - print() - - -async def test_with_mcp_tools(): - """Test with actual MCP tools if configured.""" - - print("=" * 70) - print("WORKFLOW REFLECTION AGENT TEST WITH MCP TOOLS") - print("=" * 70) - print() - - if not os.getenv("MCP_SERVER_URI"): - print("⚠ MCP_SERVER_URI not configured. Skipping MCP test.") - print("To test with MCP tools, set the MCP_SERVER_URI environment variable.") - return - - # Import the agent - try: - from agentic_ai.agents.agent_framework.multi_agent.reflection_workflow_agent import Agent - except ImportError as e: - print(f"❌ Failed to import Agent: {e}") - return - - # Create state store and agent - state_store: Dict[str, Any] = {} - session_id = "test_session_mcp_001" - - print(f"Creating Workflow Reflection Agent with MCP tools (Session: {session_id})...") - agent = Agent(state_store=state_store, session_id=session_id) - print("✓ Agent created successfully") - print() - - # Test MCP-specific queries - mcp_queries = [ - "Can you list all customers?", - "What are the billing details for customer ID 1?", - "What promotions are available for customer 1?", - ] - - for i, query in enumerate(mcp_queries, 1): - print("=" * 70) - print(f"MCP TEST QUERY {i}: {query}") - print("=" * 70) - print() - - try: - print(f"Sending query to agent (expects MCP tool usage)...") - print(f"Expected: PrimaryAgent will use MCP tools, ReviewerAgent will verify accuracy") - print() - - response = await agent.chat_async(query) - - print() - print("-" * 70) - print("FINAL RESPONSE:") - print("-" * 70) - print(response) - print() - - print("✓ MCP query completed successfully") - print() - - except Exception as e: - print(f"❌ Error during MCP query: {e}") - logger.error(f"Error during MCP query: {e}", exc_info=True) - print() - - print("=" * 70) - print("MCP TEST COMPLETE") - print("=" * 70) - - -def main(): - """Main entry point.""" - print() - print("╔═══════════════════════════════════════════════════════════════════╗") - print("║ WORKFLOW-BASED REFLECTION AGENT TEST SUITE ║") - print("╚═══════════════════════════════════════════════════════════════════╝") - print() - - # Run basic test - asyncio.run(test_workflow_reflection_agent()) - - print() - print("-" * 70) - print() - - # Run MCP test if configured - asyncio.run(test_with_mcp_tools()) - - print() - print("╔═══════════════════════════════════════════════════════════════════╗") - print("║ ALL TESTS COMPLETE ║") - print("╚═══════════════════════════════════════════════════════════════════╝") - print() - - -if __name__ == "__main__": - main() diff --git a/agentic_ai/agents/agent_framework/single_agent.py b/agentic_ai/agents/agent_framework/single_agent.py index 5b2527031..ae0a4c178 100644 --- a/agentic_ai/agents/agent_framework/single_agent.py +++ b/agentic_ai/agents/agent_framework/single_agent.py @@ -1,16 +1,15 @@ -import json import logging from typing import Any, Dict, List from agent_framework import AgentThread, ChatAgent, MCPStreamableHTTPTool from agent_framework.azure import AzureOpenAIChatClient -from agents.base_agent import BaseAgent +from agents.base_agent import BaseAgent, ToolCallTrackingMixin logger = logging.getLogger(__name__) -class Agent(BaseAgent): +class Agent(ToolCallTrackingMixin, BaseAgent): """Agent Framework implementation of a single assistant loop.""" def __init__(self, state_store: Dict[str, Any], session_id: str, access_token: str | None = None) -> None: @@ -23,6 +22,8 @@ def __init__(self, state_store: Dict[str, Any], session_id: str, access_token: s # Track conversation turn for tool call grouping - load from state store self._turn_key = f"{session_id}_current_turn" self._current_turn = state_store.get(self._turn_key, 0) + # Initialize tool tracking from mixin + self.init_tool_tracking() def set_websocket_manager(self, manager: Any) -> None: """Allow backend to inject WebSocket manager for streaming events.""" @@ -148,12 +149,17 @@ async def _log_mcp_tool_details(self) -> None: logger.debug("No tools returned from MCP server during inspection.") return + # get_tool_calls() is inherited from ToolCallTrackingMixin + async def chat_async(self, prompt: str) -> str: await self._setup_single_agent() if not self._agent or not self._thread: raise RuntimeError("Agent Framework single agent failed to initialize correctly.") + # Clear tool calls from previous request (from mixin) + self.clear_tool_calls() + # Increment turn counter for this new conversation turn and persist to state store self._current_turn += 1 self.state_store[self._turn_key] = self._current_turn @@ -162,9 +168,37 @@ async def chat_async(self, prompt: str) -> str: if self._ws_manager: return await self._chat_async_streaming(prompt) - # Non-streaming path - response = await self._agent.run(prompt, thread=self._thread) - assistant_response = response.text + # Non-streaming path - use run_stream to capture tool calls + full_response = [] + async for chunk in self._agent.run_stream(prompt, thread=self._thread): + # Extract tool calls from contents + if hasattr(chunk, 'contents') and chunk.contents: + for content in chunk.contents: + if content.type == "function_call": + # Function call chunks come in pieces: + # 1. First chunk has name, empty arguments + # 2. Subsequent chunks have no name, partial arguments + if content.name: + # New function call starting - finalize previous if any + self.track_function_call_start(content.name) + + # Accumulate arguments + args_chunk = getattr(content, 'arguments', '') + if args_chunk: + self.track_function_call_arguments(args_chunk) + + elif content.type == "function_result": + # Function result means the call is complete + self.finalize_tool_tracking() + + # Extract text + if hasattr(chunk, 'text') and chunk.text: + full_response.append(chunk.text) + + # Finalize any remaining function call + self.finalize_tool_tracking() + + assistant_response = ''.join(full_response) messages = [ {"role": "user", "content": prompt}, @@ -192,7 +226,7 @@ async def _chat_async_streaming(self, prompt: str) -> str: "show_message_in_internal_process": False, # Convention: don't show message in left panel }, ) - + # Stream the response full_response = [] @@ -201,18 +235,32 @@ async def _chat_async_streaming(self, prompt: str) -> str: # Process contents in the chunk if hasattr(chunk, 'contents') and chunk.contents: for content in chunk.contents: - # Check for tool/function calls - only broadcast the tool name + # Handle function calls - accumulate arguments across chunks if content.type == "function_call": - if self._ws_manager: - await self._ws_manager.broadcast( - self.session_id, - { - "type": "tool_called", - "agent_id": "single_agent", - "tool_name": content.name, - "turn": self._current_turn, - }, - ) + if content.name: + # New function call - finalize previous and start new + self.track_function_call_start(content.name) + + # Broadcast that a tool is being called + if self._ws_manager: + await self._ws_manager.broadcast( + self.session_id, + { + "type": "tool_called", + "agent_id": "single_agent", + "tool_name": content.name, + "turn": self._current_turn, + }, + ) + + # Accumulate arguments + args_chunk = getattr(content, 'arguments', '') + if args_chunk: + self.track_function_call_arguments(args_chunk) + + elif content.type == "function_result": + # Function completed - finalize + self.finalize_tool_tracking() # Extract text from chunk if hasattr(chunk, 'text') and chunk.text: @@ -231,6 +279,9 @@ async def _chat_async_streaming(self, prompt: str) -> str: except Exception as exc: logger.error("[STREAMING] Error during single agent streaming: %s", exc, exc_info=True) raise + + # Finalize any remaining function call + self.finalize_tool_tracking() assistant_response = ''.join(full_response) diff --git a/agentic_ai/agents/base_agent.py b/agentic_ai/agents/base_agent.py index fb8bbd6f9..7380b4ff1 100644 --- a/agentic_ai/agents/base_agent.py +++ b/agentic_ai/agents/base_agent.py @@ -1,4 +1,5 @@ import os +import json import logging from typing import Any, Dict, List, Optional, Union from dotenv import load_dotenv @@ -7,7 +8,96 @@ from azure.core.credentials import TokenCredential load_dotenv() # Load environment variables from .env file if needed - + + +class ToolCallTrackingMixin: + """ + Mixin class that provides tool call tracking functionality. + + Use this mixin in agents that need to track tool calls for evaluation. + The mixin handles: + - Accumulating streaming function call arguments + - Finalizing function calls with parsed arguments + - Providing access to tool calls made during a request + + Usage: + class MyAgent(ToolCallTrackingMixin, BaseAgent): + def __init__(self, state_store, session_id): + super().__init__(state_store, session_id) + self.init_tool_tracking() # Call this in __init__ + """ + + def init_tool_tracking(self) -> None: + """Initialize tool tracking state. Call this in agent's __init__.""" + self._tool_calls: List[Dict[str, Any]] = [] + self._current_function_call: Dict[str, Any] | None = None + self._current_function_args: List[str] = [] + + def clear_tool_calls(self) -> None: + """Clear tool calls from previous request. Call at start of chat_async.""" + self._tool_calls = [] + self._current_function_call = None + self._current_function_args = [] + + def get_tool_calls(self) -> List[Dict[str, Any]]: + """Return the list of tool calls made during the last request. + + Returns list of dicts with: + - name: tool name + - args: arguments passed to the tool + """ + return self._tool_calls.copy() + + def track_function_call_start(self, name: str) -> None: + """Start tracking a new function call. Call when function_call content is received.""" + # Finalize any previous function call first + self._finalize_current_function_call() + self._current_function_call = {"name": name} + self._current_function_args = [] + + def track_function_call_arguments(self, arguments: str) -> None: + """Accumulate streaming function call arguments.""" + if arguments: + self._current_function_args.append(arguments) + + def _finalize_current_function_call(self) -> None: + """Finalize the current function call by parsing accumulated arguments.""" + if self._current_function_call is None: + return + + # Join accumulated argument chunks + args_str = ''.join(self._current_function_args) + + # Parse the arguments + args = {} + if args_str: + try: + args = json.loads(args_str) + except json.JSONDecodeError: + # If JSON parsing fails, store raw string + args = {"_raw": args_str} if args_str.strip() else {} + + self._tool_calls.append({ + "name": self._current_function_call["name"], + "args": args + }) + + # Reset accumulators + self._current_function_call = None + self._current_function_args = [] + + def finalize_tool_tracking(self) -> None: + """Finalize any pending function calls. Call at end of streaming.""" + self._finalize_current_function_call() + + def add_tool_call(self, name: str, args: Dict[str, Any] | None = None) -> None: + """Directly add a tool call (for non-streaming scenarios).""" + self._tool_calls.append({ + "name": name, + "args": args or {} + }) + + class BaseAgent: """ Base class for all agents. diff --git a/agentic_ai/applications/backend.py b/agentic_ai/applications/backend.py index 56cf293ef..85cddef08 100644 --- a/agentic_ai/applications/backend.py +++ b/agentic_ai/applications/backend.py @@ -286,7 +286,8 @@ class ChatRequest(BaseModel): class ChatResponse(BaseModel): - response: str + response: str + tools_used: List[Dict[str, Any]] = [] # List of {name: str, args: dict} class ConversationHistoryResponse(BaseModel): @@ -325,8 +326,16 @@ async def chat(req: ChatRequest, token: str = Depends(verify_token)): agent = Agent(STATE_STORE, req.session_id, access_token=token) except TypeError: agent = Agent(STATE_STORE, req.session_id) - answer = await agent.chat_async(req.prompt) - return ChatResponse(response=answer) + answer = await agent.chat_async(req.prompt) + + # Get tool calls if the agent tracks them + tools_used = [] + if hasattr(agent, 'get_tool_calls'): + tools_used = agent.get_tool_calls() + elif hasattr(agent, '_tool_calls'): + tools_used = agent._tool_calls + + return ChatResponse(response=answer, tools_used=tools_used) @app.post("/reset_session") async def reset_session(req: SessionResetRequest, token: str = Depends(verify_token)): diff --git a/agentic_ai/applications/pyproject.toml b/agentic_ai/applications/pyproject.toml index dad4fd577..c8f6dfa01 100644 --- a/agentic_ai/applications/pyproject.toml +++ b/agentic_ai/applications/pyproject.toml @@ -5,9 +5,9 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.12" dependencies = [ - "agent-framework==1.0.0b260107", - "autogen-agentchat==0.7.1", - "autogen-ext[mcp]==0.7.1", + "agent-framework==1.0.0b260130", + "azure-ai-evaluation>=1.14.0", + "azure-ai-projects>=2.0.0b2", "azure-cosmos==4.9.0", "fastapi==0.115.12", "flasgger==0.9.7.1", @@ -26,3 +26,12 @@ dependencies = [ [tool.uv] prerelease = "allow" + +[dependency-groups] +dev = [ + "azure-identity>=1.26.0b1", + "azure-keyvault-secrets>=4.10.0", + "pytest>=9.0.2", + "pytest-asyncio>=1.3.0", + "pytest-timeout>=2.4.0", +] diff --git a/agentic_ai/applications/uv.lock b/agentic_ai/applications/uv.lock index 5fff23e52..022275724 100644 --- a/agentic_ai/applications/uv.lock +++ b/agentic_ai/applications/uv.lock @@ -2,7 +2,8 @@ version = 1 revision = 1 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.13'", + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", "python_full_version < '3.13'", ] @@ -39,14 +40,14 @@ wheels = [ [[package]] name = "agent-framework" -version = "1.0.0b260107" +version = "1.0.0b260130" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "agent-framework-core", extra = ["all"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7e/e7/5ad52075da4e586ca94fb8806b3085ac5dea8059413e413bff88c0452e88/agent_framework-1.0.0b260107.tar.gz", hash = "sha256:a2f6508a0ca1df3b7ca4e3a64e45bac8e33cdfe02cf69e9056e37e881a58aad7", size = 2898189 } +sdist = { url = "https://files.pythonhosted.org/packages/93/10/ba51bf04ea2900897a221664e4e673dcc7a7a58a6658eeb85115e920d9b4/agent_framework-1.0.0b260130.tar.gz", hash = "sha256:50e13b74366b8092cb81769f07b3b42d6ddc8888a51244933c3214df591b7108", size = 3506765 } wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/55/ffef27526cc26bf163ccf9d58ba87bf4e677bba343a542e7b666846f744d/agent_framework-1.0.0b260107-py3-none-any.whl", hash = "sha256:080deb32bff4ef07227a4ba709798c67079ff8a2997fe7a0aed0010adc0c18cf", size = 5554 }, + { url = "https://files.pythonhosted.org/packages/bb/3d/2a8efa9085c7fec503a64038f986faf0cdf7f5de853c4ae30724e2e2bda6/agent_framework-1.0.0b260130-py3-none-any.whl", hash = "sha256:b9ba1487f91ab22031e01b5c09e5649181fd717f807d94f22ec43a409c43cde1", size = 5552 }, ] [[package]] @@ -160,7 +161,7 @@ wheels = [ [[package]] name = "agent-framework-core" -version = "1.0.0b260107" +version = "1.0.0b260130" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-identity" }, @@ -174,9 +175,9 @@ dependencies = [ { name = "pydantic-settings" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9d/44/06f5d2c99dd7bdb82c2cb5cbc354b5bc6af72d1886d20eff1dff83508fae/agent_framework_core-1.0.0b260107.tar.gz", hash = "sha256:12636fb64664c6153546f0d85dafccdbe57226767c14b3f38985867389f980bb", size = 3574757 } +sdist = { url = "https://files.pythonhosted.org/packages/4d/39/e508e778219bd6d20e023a6f48235861a639e3cf888776f9e873bbad3c6b/agent_framework_core-1.0.0b260130.tar.gz", hash = "sha256:030a5b2ced796eec6839c2dabad90b4bd1ea33d1026f3ed1813050a56ccfa4ec", size = 301823 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5a/8c6315a2ca119ad48340344616d4b8e77fd68e2892f82c402069a52ad647/agent_framework_core-1.0.0b260107-py3-none-any.whl", hash = "sha256:5bd119b8d30dc2d5bee1c4a5c3597d7afc808a52e4de148725c4f2d9bcc7632b", size = 5687298 }, + { url = "https://files.pythonhosted.org/packages/36/68/afe66c72951a279e0fe048fd5af1e775528cde40dbdab8ec03b42c545df4/agent_framework_core-1.0.0b260130-py3-none-any.whl", hash = "sha256:75b4dd0ca2ae52574d406cf5c9ed7adf63e187379f72fce891743254d83dfd56", size = 348724 }, ] [package.optional-dependencies] @@ -191,6 +192,8 @@ all = [ { name = "agent-framework-copilotstudio" }, { name = "agent-framework-declarative" }, { name = "agent-framework-devui" }, + { name = "agent-framework-durabletask" }, + { name = "agent-framework-github-copilot" }, { name = "agent-framework-lab" }, { name = "agent-framework-mem0" }, { name = "agent-framework-ollama" }, @@ -227,6 +230,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/21/359592eda88e88d8215a5941d120935588bbb7454336514c5353b4ae6240/agent_framework_devui-1.0.0b251114-py3-none-any.whl", hash = "sha256:75657a4b14de5271c587d5ef130d7c031b5936785c3283e16f66b871b1ffa278", size = 338108 }, ] +[[package]] +name = "agent-framework-durabletask" +version = "1.0.0b260130" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "durabletask" }, + { name = "durabletask-azuremanaged" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/95/9d5ee7fd1fdcd52c10aa1b2902964701d1d62b9d35cc7d05115b90db6329/agent_framework_durabletask-1.0.0b260130.tar.gz", hash = "sha256:63a2c8e0968a51d8e132892e9d385d2b82ccb95263d2c0316dc46b0eaa4dd7a4", size = 30285 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/22/122ed515935926137cc3c6ca795ef01b30feb82160cfc0f29a34f9d603de/agent_framework_durabletask-1.0.0b260130-py3-none-any.whl", hash = "sha256:a46e292800d10a62ce0923efe753594ddbf0bd6d1bb6e1258380f0dbf7d0302f", size = 36357 }, +] + +[[package]] +name = "agent-framework-github-copilot" +version = "1.0.0b260130" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "agent-framework-core" }, + { name = "github-copilot-sdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/00/f69d731db02e256b8d18d6d8cd20d3d0684245df876f22b836743403a9c1/agent_framework_github_copilot-1.0.0b260130.tar.gz", hash = "sha256:3f5f231785bc8e663da2d1db65a5e4ee49a0f6266e31cccbf3ef05a79ab6c90d", size = 7929 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/b8/0a09396682e915dc25dc39c69fc06cc199b9901ccb0fdbb5e9e2886d2cb0/agent_framework_github_copilot-1.0.0b260130-py3-none-any.whl", hash = "sha256:b8844bacbf666ff1ea7f27d34a42c11be4ade1c4d57e7545341bb74462d82703", size = 8752 }, +] + [[package]] name = "agent-framework-lab" version = "1.0.0b251024" @@ -465,6 +496,8 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "agent-framework" }, + { name = "azure-ai-evaluation" }, + { name = "azure-ai-projects" }, { name = "azure-cosmos" }, { name = "fastapi" }, { name = "flasgger" }, @@ -481,9 +514,20 @@ dependencies = [ { name = "websockets" }, ] +[package.dev-dependencies] +dev = [ + { name = "azure-identity" }, + { name = "azure-keyvault-secrets" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-timeout" }, +] + [package.metadata] requires-dist = [ - { name = "agent-framework", specifier = "==1.0.0b260107" }, + { name = "agent-framework", specifier = "==1.0.0b260130" }, + { name = "azure-ai-evaluation", specifier = ">=1.14.0" }, + { name = "azure-ai-projects", specifier = ">=2.0.0b2" }, { name = "azure-cosmos", specifier = "==4.9.0" }, { name = "fastapi", specifier = "==0.115.12" }, { name = "flasgger", specifier = "==0.9.7.1" }, @@ -500,6 +544,24 @@ requires-dist = [ { name = "websockets", specifier = ">=15.0.1" }, ] +[package.metadata.requires-dev] +dev = [ + { name = "azure-identity", specifier = ">=1.26.0b1" }, + { name = "azure-keyvault-secrets", specifier = ">=4.10.0" }, + { name = "pytest", specifier = ">=9.0.2" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "pytest-timeout", specifier = ">=2.4.0" }, +] + +[[package]] +name = "asyncio" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/ea/26c489a11f7ca862d5705db67683a7361ce11c23a7b98fc6c2deaeccede2/asyncio-4.0.0.tar.gz", hash = "sha256:570cd9e50db83bc1629152d4d0b7558d6451bb1bfd5dfc2e935d96fc2f40329b", size = 5371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/64/eff2564783bd650ca25e15938d1c5b459cda997574a510f7de69688cb0b4/asyncio-4.0.0-py3-none-any.whl", hash = "sha256:c1eddb0659231837046809e68103969b2bef8b0400d59cfa6363f6b5ed8cc88b", size = 5555 }, +] + [[package]] name = "attrs" version = "25.4.0" @@ -523,6 +585,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/6d/15070d23d7a94833a210da09d5d7ed3c24838bb84f0463895e5d159f1695/azure_ai_agents-1.2.0b5-py3-none-any.whl", hash = "sha256:257d0d24a6bf13eed4819cfa5c12fb222e5908deafb3cbfd5711d3a511cc4e88", size = 217948 }, ] +[[package]] +name = "azure-ai-evaluation" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "azure-core" }, + { name = "azure-identity" }, + { name = "azure-storage-blob" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "msrest" }, + { name = "nltk" }, + { name = "openai" }, + { name = "pandas" }, + { name = "pyjwt" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/ab/62300008df848b210ef2a21b646480eee7c1bf3906afdc1351795343321c/azure_ai_evaluation-1.14.0.tar.gz", hash = "sha256:2a5681805b7cde65ad663f34d0f647d28498dd9395f7e2ce0789320c26664dae", size = 2196726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/68/1e0bd2123a5e681dbe474a3dda098c85704556e53ae24c7f4b3915d4e048/azure_ai_evaluation-1.14.0-py3-none-any.whl", hash = "sha256:1785f9be28517839ab9d30a03893951f7c9b530500d939d0ae51dde3aa1478b0", size = 1141136 }, +] + [[package]] name = "azure-ai-projects" version = "2.0.0b2" @@ -619,6 +704,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e1/28/af9ef022f21e3b51b3718d4348f771b490678c1116563895547c0a771362/azure_identity-1.26.0b1-py3-none-any.whl", hash = "sha256:dc608b59ae628a38611208ee761adeb1a2b9390258b58d6edcda2d24c50a4348", size = 197227 }, ] +[[package]] +name = "azure-keyvault-secrets" +version = "4.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "isodate" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/97/e5/3074e581b6e8923c4a1f2e42192ea6f390bb52de3600c68baaaed529ef05/azure_keyvault_secrets-4.10.0.tar.gz", hash = "sha256:666fa42892f9cee749563e551a90f060435ab878977c95265173a8246d546a36", size = 129695 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/94/7c902e966b28e7cb5080a8e0dd6bffc22ba44bc907f09c4c633d2b7c4f6a/azure_keyvault_secrets-4.10.0-py3-none-any.whl", hash = "sha256:9dbde256077a4ee1a847646671580692e3f9bea36bcfc189c3cf2b9a94eb38b9", size = 125237 }, +] + [[package]] name = "azure-search-documents" version = "11.7.0b2" @@ -816,7 +915,7 @@ name = "clr-loader" version = "0.2.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, + { name = "cffi", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605 } wheels = [ @@ -885,6 +984,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 }, ] +[[package]] +name = "durabletask" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asyncio" }, + { name = "grpcio" }, + { name = "packaging" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/27/3d021e6b36fc1aab6099fafc56dfc8059b4e8968615a26c1a0418601e50a/durabletask-1.3.0.tar.gz", hash = "sha256:11e38dda6df4737fadca0c71fc0a0f769955877c8a8bdb25ccbf90cf45afbf63", size = 57830 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/87/31ea460dbfaf50d9877f143e2ce9829cac2fb106747d9900cc353356ea77/durabletask-1.3.0-py3-none-any.whl", hash = "sha256:411f23e13391b8845edca010873dd7a87ee7cfc1fe05753ab28a7cd7c3c1bd77", size = 64112 }, +] + +[[package]] +name = "durabletask-azuremanaged" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-identity" }, + { name = "durabletask" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/29/6bb0b5fe51aa92e117adcdc93efe97cf5476d86c1496e5c5ab35d99a8d07/durabletask_azuremanaged-1.3.0.tar.gz", hash = "sha256:55172588e075afa80d46dcc2e5ddbd84be0a20cc78c74f687040c3720677d34c", size = 4343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/11/4d34fec302c4813e626080f1532d189767eb31d6d80e8f3698c230512f14/durabletask_azuremanaged-1.3.0-py3-none-any.whl", hash = "sha256:9da914f569da1597c858d494a95eda37e4372726c0ee65f30080dcafab262d60", size = 6366 }, +] + [[package]] name = "fastapi" version = "0.115.12" @@ -1043,6 +1170,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, ] +[[package]] +name = "github-copilot-sdk" +version = "0.1.20" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dateutil" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/02/7d/afde0ec85815a558612130dc5ff79536299f411e672410c3edc0c1edeb2a/github_copilot_sdk-0.1.20.tar.gz", hash = "sha256:9e89cd46577fd18dd808d7113b7e20e021c4f944121a0a4891945460fb26c53c", size = 92207 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/91/f8cfa809184988a273af58824b312d31a532ee3ee70875100b5061540178/github_copilot_sdk-0.1.20-py3-none-any.whl", hash = "sha256:e7fa1bb843e2494930126551b80f3a035f36c47a05f9173ad0cdfb4151ad9346", size = 40306 }, +] + [[package]] name = "gitpython" version = "3.1.45" @@ -1321,6 +1462,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656 }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + [[package]] name = "isodate" version = "0.7.2" @@ -1419,6 +1569,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 }, ] +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071 }, +] + [[package]] name = "jsonpath-ng" version = "1.7.0" @@ -1676,6 +1835,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, ] +[[package]] +name = "msrest" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "azure-core" }, + { name = "certifi" }, + { name = "isodate" }, + { name = "requests" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384 }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -1784,6 +1959,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/a1/4d21933898e23b011ae0528151b57a9230a62960d0919bf2ee48c7f5c20a/narwhals-2.11.0-py3-none-any.whl", hash = "sha256:a9795e1e44aa94e5ba6406ef1c5ee4c172414ced4f1aea4a79e5894f0c7378d4", size = 423069 }, ] +[[package]] +name = "nltk" +version = "3.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404 }, +] + [[package]] name = "numpy" version = "2.3.5" @@ -1847,6 +2037,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459 }, ] +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 }, +] + [[package]] name = "ollama" version = "0.6.1" @@ -2096,6 +2295,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + [[package]] name = "ply" version = "3.11" @@ -2139,8 +2347,8 @@ name = "powerfx" version = "0.0.34" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cffi" }, - { name = "pythonnet" }, + { name = "cffi", marker = "python_full_version < '3.14'" }, + { name = "pythonnet", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555 } wheels = [ @@ -2414,6 +2622,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/4c/b888e6cf58bd9db9c93f40d1c6be8283ff49d88919231afe93a6bcf61626/pydeck-0.9.1-py2.py3-none-any.whl", hash = "sha256:b3f75ba0d273fc917094fa61224f3f6076ca8752b93d46faf3bcfd9f9d59b038", size = 6900403 }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + [[package]] name = "pyjwt" version = "2.10.1" @@ -2428,6 +2645,47 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, +] + +[[package]] +name = "pytest-timeout" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2472,7 +2730,7 @@ name = "pythonnet" version = "3.0.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "clr-loader" }, + { name = "clr-loader", marker = "python_full_version < '3.14'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212 } wheels = [ @@ -2610,6 +2868,94 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775 }, ] +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398 }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339 }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003 }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656 }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252 }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268 }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589 }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700 }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928 }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607 }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729 }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697 }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849 }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279 }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166 }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415 }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164 }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218 }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895 }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680 }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210 }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358 }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583 }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782 }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978 }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550 }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747 }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615 }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951 }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275 }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145 }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411 }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068 }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756 }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114 }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524 }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455 }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007 }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794 }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159 }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558 }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427 }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939 }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753 }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559 }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879 }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317 }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551 }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170 }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146 }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986 }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098 }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980 }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607 }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358 }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833 }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045 }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374 }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940 }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112 }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586 }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691 }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422 }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467 }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073 }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757 }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122 }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761 }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538 }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066 }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938 }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314 }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652 }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550 }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981 }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780 }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778 }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667 }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386 }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837 }, +] + [[package]] name = "requests" version = "2.32.4" @@ -2625,6 +2971,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847 }, ] +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, +] + [[package]] name = "rpds-py" version = "0.29.0" @@ -2718,6 +3077,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, ] +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102 }, +] + [[package]] name = "six" version = "1.17.0" diff --git a/agentic_ai/evaluations/.gitignore b/agentic_ai/evaluations/.gitignore new file mode 100644 index 000000000..1d2876728 --- /dev/null +++ b/agentic_ai/evaluations/.gitignore @@ -0,0 +1,10 @@ +# Evaluation results directory +eval_results/ +*.pyc +__pycache__/ +.pytest_cache/ +*.json.bak +agent_traces.json + +# Generated evaluation data (created by run_agent_eval.py) +evaluation_input_data.jsonl diff --git a/agentic_ai/evaluations/README.md b/agentic_ai/evaluations/README.md new file mode 100644 index 000000000..209542b43 --- /dev/null +++ b/agentic_ai/evaluations/README.md @@ -0,0 +1,621 @@ +# AI Agent Evaluation Framework + +A comprehensive evaluation system for testing AI agents in customer support scenarios. This framework provides both **local evaluation** with custom metrics and **remote evaluation** via Azure AI Foundry with LLM-as-judge capabilities. + +--- + +## Table of Contents + +1. [Evaluation Methodology](#evaluation-methodology) + - [Why Evaluate AI Agents?](#why-evaluate-ai-agents) + - [Single-Turn vs Multi-Turn Evaluation](#single-turn-vs-multi-turn-evaluation) + - [Built-in vs Custom Evaluators](#built-in-vs-custom-evaluators) +2. [Metrics Deep Dive](#metrics-deep-dive) + - [Single-Turn Metrics (Tool-Focused)](#single-turn-metrics-tool-focused) + - [Multi-Turn Metrics (Outcome-Focused)](#multi-turn-metrics-outcome-focused) +3. [Setup Guide](#setup-guide) + - [Prerequisites](#prerequisites) + - [Step 1: Environment Setup](#step-1-environment-setup) + - [Step 2: Azure Configuration](#step-2-azure-configuration) + - [Step 3: Start Services](#step-3-start-services) +4. [Running Evaluations](#running-evaluations) + - [Local Evaluation](#local-evaluation) + - [Remote Evaluation (Azure AI Foundry)](#remote-evaluation-azure-ai-foundry) + - [Comparing Agents](#comparing-agents) +5. [Interpreting Results](#interpreting-results) +6. [Extending the Framework](#extending-the-framework) +7. [Troubleshooting](#troubleshooting) + +--- + +## Evaluation Methodology + +### Why Evaluate AI Agents? + +AI agents that use tools (APIs, databases, external services) require evaluation that goes beyond traditional NLP metrics. Unlike simple chatbots, agents must: + +1. **Choose the right tools** - Select appropriate APIs for each task +2. **Use tools correctly** - Pass correct parameters and handle responses +3. **Maintain context** - Remember information across conversation turns +4. **Achieve outcomes** - Actually solve the customer's problem +5. **Communicate effectively** - Provide clear, helpful responses + +This framework addresses all these dimensions through a combination of **rule-based metrics** (deterministic, fast) and **LLM-as-judge evaluators** (semantic understanding, nuanced assessment). + +### Single-Turn vs Multi-Turn Evaluation + +We use fundamentally different evaluation strategies for single-turn and multi-turn conversations because they measure different capabilities: + +#### Single-Turn Evaluation (Tool-Focused) + +**Rationale**: In a single exchange, the agent must immediately demonstrate correct tool selection and usage. There's no opportunity for course correction, so we heavily weight tool-level accuracy. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ SINGLE-TURN WEIGHTS │ +├─────────────────────────────────────────────────────────────┤ +│ Tool Behavior (recall, precision, efficiency) │ 10% │ +│ Tool Call Accuracy (LLM-judge) │ 15% │ +│ Task Adherence (LLM-judge) │ 10% │ +│ Completeness (success criteria met) │ 10% │ +│ Response Quality - LLM │ 10% │ +│ Response Quality - Basic │ 5% │ +│ Grounded Accuracy │ 10% │ +│ Intent Resolution │ 10% │ +│ Coherence │ 5% │ +│ Fluency │ 5% │ +│ Relevance │ 5% │ +│ Solution Accuracy │ 10% │ ← Ground truth match +└─────────────────────────────────────────────────────────────┘ +``` + +**Use cases**: Quick lookups, simple queries, one-shot requests + +#### Multi-Turn Evaluation (Outcome-Focused) + +**Rationale**: In multi-turn conversations, what matters is the **final outcome**, not the path taken. An agent might take different tool sequences across turns but still successfully resolve the customer's issue. Penalizing intermediate tool choices would be counterproductive. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ MULTI-TURN WEIGHTS │ +├─────────────────────────────────────────────────────────────┤ +│ Solution Accuracy │ 30% │ ← Did we solve it? +│ Task Adherence │ 20% │ ← Proper procedure? +│ Intent Resolution │ 20% │ ← All intents handled? +│ Coherence │ 10% │ ← Logical conversation? +│ Fluency │ 10% │ ← Quality communication? +│ Relevance │ 10% │ ← Stayed on topic? +├─────────────────────────────────────────────────────────────┤ +│ ❌ Tool metrics EXCLUDED - we care about outcomes │ +└─────────────────────────────────────────────────────────────┘ +``` + +**Use cases**: Complex problem resolution, account changes requiring multiple steps, escalation flows + +### Built-in vs Custom Evaluators + +We combine two types of evaluators to get the best of both worlds: + +#### Azure AI Foundry Built-in Evaluators + +**Why use them:** +- ✅ Industry-standard LLM-as-judge implementations +- ✅ Consistent with Azure AI Foundry portal metrics +- ✅ Maintained and improved by Microsoft +- ✅ 1-5 scale matching portal visualization + +| Evaluator | What it Measures | +|-----------|------------------| +| `IntentResolutionEvaluator` | Did the agent understand the customer's intent? | +| `TaskAdherenceEvaluator` | Did the agent follow proper procedures? | +| `ToolCallAccuracyEvaluator` | Were tool calls correct and appropriate? | +| `CoherenceEvaluator` | Was the response logically structured? | +| `FluencyEvaluator` | Was the language natural and grammatically correct? | +| `RelevanceEvaluator` | Was the response relevant to the question? | + +#### Custom Evaluators + +**Why we need them:** +- 🔧 Domain-specific logic (e.g., billing-specific success criteria) +- 🎯 Ground truth matching against expected solutions +- ⚡ Deterministic rules (fast, no API calls needed) +- 📊 Tool behavior metrics unique to our agent patterns + +| Evaluator | What it Measures | Why Custom? | +|-----------|------------------|-------------| +| `ToolBehaviorEvaluator` | Recall, precision, efficiency of tool usage | Requires domain knowledge of expected tools | +| `CompletenessEvaluator` | Success criteria satisfaction | Maps criteria to specific tool requirements | +| `GroundedAccuracyEvaluator` | Response grounded in tool outputs | Needs access to tool call results | +| `SolutionAccuracyEvaluator` | Match against ground truth | Uses scenario-specific rubrics | + +#### Score Scale: 1-5 + +All metrics use a **1-5 scale** with a **threshold of 3** for pass/fail: + +``` +Score Meaning Pass? +───── ───────────────── ───── + 5 Excellent ✓ + 4 Good ✓ + 3 Acceptable ✓ (threshold) + 2 Below expectations ✗ + 1 Poor ✗ +``` + +This matches the Azure AI Foundry portal visualization, making local and remote evaluation results directly comparable. + +--- + +## Metrics Deep Dive + +### Single-Turn Metrics (Tool-Focused) + +#### 1. Tool Behavior (10%) +Combines three sub-metrics: +- **Recall** (50%): Fraction of required tools actually used +- **Precision** (30%): Fraction of used tools that were relevant +- **Efficiency** (20%): Required tools / total tools used + +```python +# Example: Required [get_billing_summary], Used [get_billing_summary, get_customer_detail] +recall = 1.0 # 1/1 required tools used +precision = 0.5 # 1/2 used tools were in expected set +efficiency = 0.5 # 1/2 ratio +``` + +#### 2. Tool Call Accuracy (15%) - LLM Judge +Azure AI Foundry's `ToolCallAccuracyEvaluator` assesses: +- Were the correct tools selected? +- Were parameters passed correctly? +- Was the sequence appropriate? + +#### 3. Task Adherence (10%) - LLM Judge +Evaluates whether the agent followed proper procedures and policies. + +#### 4. Completeness (10%) +Checks if scenario-specific success criteria were met: +```python +TOOL_CRITERIA_MAP = { + "must_access_billing": ["get_billing_summary", "get_subscription_detail"], + "must_check_security_logs": ["get_security_logs"], + "must_check_promotions": ["get_eligible_promotions"], +} +``` + +#### 5. Response Quality (15% total) +- **LLM-based** (10%): Semantic quality assessment +- **Basic** (5%): Length, formatting, structure checks + +#### 6. Grounded Accuracy (10%) +Verifies response is consistent with tool outputs (no hallucination). + +#### 7. Intent Resolution (10%) - LLM Judge +Did the agent correctly understand what the customer wanted? + +#### 8-10. Coherence, Fluency, Relevance (5% each) +Standard NLG quality metrics via Azure AI Foundry evaluators. + +#### 11. Solution Accuracy (10%) +Compares agent response against expected ground truth solution. + +### Multi-Turn Metrics (Outcome-Focused) + +For multi-turn conversations, we **exclude tool-level metrics** and focus on outcomes: + +| Metric | Weight | Rationale | +|--------|--------|-----------| +| Solution Accuracy | 30% | The ultimate measure - did we solve the problem? | +| Task Adherence | 20% | Did we follow proper procedures throughout? | +| Intent Resolution | 20% | Were all customer intents (across turns) resolved? | +| Coherence | 10% | Was the overall conversation logical and consistent? | +| Fluency | 10% | Was communication quality maintained? | +| Relevance | 10% | Did responses stay relevant across all turns? | + +**Why exclude tool metrics for multi-turn?** + +Consider a billing dispute that spans 3 turns: +1. Customer asks about high bill → Agent retrieves billing summary +2. Customer asks about specific charge → Agent gets usage data +3. Customer requests payment plan → Agent records payment + +Evaluating tool accuracy at each turn is misleading because: +- The "expected" tools depend on previous turn outcomes +- Alternative valid tool sequences exist +- What matters is: **Was the dispute resolved?** + +--- + +## Setup Guide + +### Prerequisites + +- Python 3.10+ with `uv` package manager +- Azure CLI authenticated (`az login`) +- Existing `.env` file configured (see main repo [SETUP.md](../../SETUP.md)) +- Azure subscription with: + - Azure OpenAI resource (already configured in your `.env`) + - Azure AI Project (for remote evaluation) + +### Step 1: Environment Setup + +If you haven't already set up the repository: + +```bash +# Clone repository +git clone https://github.com/microsoft/OpenAIWorkshop.git +cd OpenAIWorkshop + +# Install dependencies +uv sync +``` + +### Step 2: Configure Evaluation Variables + +Add these variables to your existing `.env` file in `agentic_ai/applications/`: + +```bash +# ============================================================ +# EVALUATION-SPECIFIC CONFIGURATION (add to existing .env) +# ============================================================ + +# Azure AI Foundry Project Endpoint (Required for --remote evaluation) +# Get this from: https://ai.azure.com → Your Project → Settings → Project details +# Look for "Project endpoint" in the format: +# https://.api.azureml.ms/... (older projects) +# https://.services.ai.azure.com/api/projects/ (newer projects) +AZURE_AI_PROJECT_ENDPOINT=https://your-account.services.ai.azure.com/api/projects/your-project + +# Evaluation Model (Optional - defaults to AZURE_OPENAI_CHAT_DEPLOYMENT) +# Use a separate deployment for evaluation to avoid rate limiting +# Recommended: gpt-4o or gpt-4o-mini for reliable LLM-as-judge evaluation +AZURE_OPENAI_EVAL_DEPLOYMENT=gpt-4o-mini +``` + +**Where to find the Project Endpoint:** +1. Go to [Azure AI Foundry](https://ai.azure.com) +2. Select your project +3. Click **Settings** → **Project details** +4. Copy the **Project endpoint** URL + +> **Note**: The evaluation uses your existing `AZURE_OPENAI_CHAT_DEPLOYMENT` if `AZURE_OPENAI_EVAL_DEPLOYMENT` is not set. Consider using a separate deployment for evaluation to avoid rate limiting during heavy testing. + +**Assign required Azure roles:** +```bash +# Azure AI Developer role (required for remote evaluation) +az role assignment create \ + --assignee $(az ad signed-in-user show --query id -o tsv) \ + --role "Azure AI Developer" \ + --scope /subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.CognitiveServices/accounts/{ai-project} +``` + +### Step 3: Start Services + +Start services in this order: + +```bash +# Terminal 1: MCP Server (provides customer data APIs) +cd mcp +uv run python mcp_service.py +# Wait for: "MCP server running on http://localhost:8000" + +# Terminal 2: Agent Backend +cd agentic_ai/applications +uv run python -m uvicorn backend:app --port 7000 --reload +# Wait for: "Application startup complete" +``` + +Verify services: +```bash +curl http://localhost:8000/health # MCP server +curl http://localhost:7000/health # Backend +``` + +--- + +## Running Evaluations + +### Command-Line Options + +```bash +cd agentic_ai/applications + +uv run python ../evaluations/run_agent_eval.py [OPTIONS] +``` + +| Flag | Description | +|------|-------------| +| `--agent NAME` | Agent name for tracking (default: from AGENT_MODULE) | +| `--backend-url URL` | Backend URL (default: http://localhost:7000) | +| `--local` | Run local evaluation only (default if neither specified) | +| `--remote` | Push results to Azure AI Foundry | +| `--single-turn-only` | Run only single-turn test cases | +| `--multi-turn-only` | Run only multi-turn test cases | +| `--limit N` | Limit to N test cases (useful for testing) | + +### Local Evaluation + +Local evaluation runs custom metrics without Azure AI Foundry: + +```bash +# Basic local evaluation (all test cases) +uv run python ../evaluations/run_agent_eval.py --agent my_agent + +# Single-turn only +uv run python ../evaluations/run_agent_eval.py --agent my_agent --single-turn-only + +# Multi-turn only +uv run python ../evaluations/run_agent_eval.py --agent my_agent --multi-turn-only + +# Quick test with 2 cases +uv run python ../evaluations/run_agent_eval.py --agent my_agent --limit 2 +``` + +**Output:** +``` +================================================================================ +EVALUATION SUMMARY - http://localhost:7000 +================================================================================ +Agent: my_agent +Total Tests: 30 +Passed: 26 ✓ +Failed: 4 ✗ +Pass Rate: 86.7% +Average Score: 4.12 + +Metric Breakdown (1-5 scale, threshold: 3): + tool_behavior : 4.2/5 ████████████████ ✓ + completeness : 4.5/5 ██████████████████ ✓ + solution_accuracy : 3.8/5 ███████████████ ✓ + coherence : 4.6/5 ██████████████████ ✓ + ... +``` + +### Remote Evaluation (Azure AI Foundry) + +Remote evaluation pushes results to Azure AI Foundry portal: + +```bash +# Remote only (skip local evaluation) +uv run python ../evaluations/run_agent_eval.py --agent my_agent --remote + +# Both local and remote +uv run python ../evaluations/run_agent_eval.py --agent my_agent --local --remote +``` + +**What happens:** +1. Runs test cases against agent backend +2. Generates `evaluation_input_data.jsonl` in Foundry format +3. Creates evaluation in Azure AI Foundry with built-in evaluators: + - `builtin.coherence` + - `builtin.fluency` + - `builtin.relevance` + - `builtin.groundedness` + - `builtin.task_adherence` + - `builtin.intent_resolution` + - Custom `label_model` for solution_accuracy + +**Portal naming convention:** +- Evaluation: `my_agent - Single Turn | 2026-02-03 14:30` +- Run: `my_agent | Single Turn | 2026-02-03 14:30` + +### Comparing Agents + +Compare different agent implementations: + +```bash +# Compare single vs reflection agents +uv run python ../evaluations/run_agent_eval.py --agent agent_single --remote +# Restart backend with reflection agent +uv run python ../evaluations/run_agent_eval.py --agent agent_reflection --remote +``` + +View comparison in Azure AI Foundry portal → Evaluations → Compare runs. + +--- + +## Interpreting Results + +### Score Thresholds + +| Score Range | Meaning | Action | +|-------------|---------|--------| +| 4.5 - 5.0 | Excellent | Agent performing optimally | +| 3.5 - 4.4 | Good | Minor improvements possible | +| 3.0 - 3.4 | Acceptable | Investigate low-scoring metrics | +| 2.0 - 2.9 | Below expectations | Requires attention | +| 1.0 - 1.9 | Poor | Significant issues to fix | + +### Common Issues + +**Low Tool Behavior Score:** +- Agent using unnecessary tools (low efficiency) +- Missing required tools (low recall) +- Fix: Review agent's tool selection logic + +**Low Solution Accuracy:** +- Agent response doesn't match expected outcome +- Fix: Check ground truth in dataset, verify agent logic + +**Low Coherence/Fluency:** +- Response structure or language issues +- Fix: Adjust system prompts for clearer formatting + +### Output Files + +| File | Description | +|------|-------------| +| `eval_results/evaluation_summary.json` | Aggregate scores and pass rates | +| `eval_results/test_case_results.json` | Per-test-case detailed results | +| `evaluation_input_data.jsonl` | Foundry-format data for remote evaluation | + +--- + +## Extending the Framework + +### Adding Custom Metrics + +1. Create evaluator class in `metrics.py`: + +```python +class MyCustomEvaluator: + def evaluate(self, response: str, expected: str) -> EvaluationResult: + # Your evaluation logic + score = ... # 1-5 scale + return EvaluationResult( + metric_name="my_metric", + metric_type=MetricType.ACCURACY, + score=score, + passed=score >= 3.0, + details={...}, + explanation="..." + ) +``` + +2. Add to `evaluator.py` weights: +```python +SINGLE_TURN_WEIGHTS = { + ... + "my_metric": 0.05, # 5% weight +} +``` + +### Adding Test Cases + +Add to `eval_dataset.json`: + +```json +{ + "id": "billing_new_scenario", + "customer_query": "Your test query here", + "customer_id": 101, + "category": "billing", + "expected_tools": ["get_billing_summary"], + "required_tools": ["get_billing_summary"], + "success_criteria": {"must_access_billing": true}, + "ground_truth_solution": "Expected agent response...", + "scoring_rubric": "5: Complete and accurate...", + "multi_turn": false +} +``` + +--- + +## Troubleshooting + +### "Missing AZURE_AI_PROJECT_ENDPOINT" +```bash +# Add the project endpoint to your .env file +# Get it from: https://ai.azure.com → Your Project → Settings → Project details +echo 'AZURE_AI_PROJECT_ENDPOINT=https://your-account.services.ai.azure.com/api/projects/your-project' >> agentic_ai/applications/.env +``` + +### "Failed to resolve hostname" / DNS Error +```bash +# Placeholder values in .env file - must use real URLs +grep "AZURE_AI_PROJECT" agentic_ai/applications/.env +# Should show your actual Azure endpoint, not placeholders like "your-account" +``` + +### "Authentication failed" +```bash +az login +az account show +# Verify Azure AI Developer role is assigned to your account +``` + +### "Cannot connect to backend" +```bash +# Check services are running +curl http://localhost:8000/health # MCP +curl http://localhost:7000/health # Backend +``` + +### "No evaluation results in Foundry" +- Verify `--remote` flag was used +- Check `AZURE_AI_PROJECT_ENDPOINT` is set correctly +- Wait 1-2 minutes for portal to update + +### "Rate limiting" during evaluation +```bash +# Use a separate deployment for evaluation +# Add to your .env: +AZURE_OPENAI_EVAL_DEPLOYMENT=gpt-4o-mini-eval +# This avoids sharing quota with your agent's chat deployment +``` + +### Low Scores on All Tests +- Verify MCP server has test data loaded +- Check agent can access tools (`DISABLE_AUTH=true` in dev) +- Review agent logs for errors + +--- + +## Environment Variables Reference + +| Variable | Required | Description | +|----------|----------|-------------| +| `AZURE_AI_PROJECT_ENDPOINT` | For `--remote` | Azure AI Foundry project endpoint URL | +| `AZURE_OPENAI_EVAL_DEPLOYMENT` | No | Model deployment for LLM-as-judge (defaults to `AZURE_OPENAI_CHAT_DEPLOYMENT`) | +| `AZURE_OPENAI_CHAT_DEPLOYMENT` | Yes | Default model deployment (used if eval deployment not set) | +| `AZURE_OPENAI_ENDPOINT` | Yes | Azure OpenAI resource endpoint | +| `AZURE_OPENAI_API_KEY` | Yes* | Azure OpenAI API key (*or use managed identity) | + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ Evaluation Framework │ +├─────────────────────────────────────────────────────────────────────────┤ +│ │ +│ run_agent_eval.py │ +│ ├── Load eval_dataset.json (30 test cases) │ +│ ├── Send queries to Agent Backend (HTTP) │ +│ ├── Capture tool calls via WebSocket │ +│ └── Run evaluators │ +│ │ │ +│ ├── LOCAL EVALUATION (evaluator.py + metrics.py) │ +│ │ ├── ToolBehaviorEvaluator (recall, precision, efficiency) │ +│ │ ├── CompletenessEvaluator (success criteria) │ +│ │ ├── ResponseQualityEvaluator (LLM + basic) │ +│ │ ├── GroundedAccuracyEvaluator │ +│ │ └── AzureAIEvaluatorSuite (if SDK available) │ +│ │ ├── IntentResolutionEvaluator │ +│ │ ├── TaskAdherenceEvaluator │ +│ │ ├── ToolCallAccuracyEvaluator │ +│ │ ├── CoherenceEvaluator │ +│ │ ├── FluencyEvaluator │ +│ │ └── RelevanceEvaluator │ +│ │ │ +│ └── REMOTE EVALUATION (Azure AI Foundry) │ +│ ├── Upload evaluation_input_data.jsonl │ +│ ├── Run builtin.* evaluators │ +│ └── Run label_model for solution_accuracy │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## File Reference + +| File | Purpose | +|------|---------| +| `run_agent_eval.py` | Main evaluation script - orchestrates tests, local eval, and remote push | +| `evaluator.py` | Evaluation runner, weight definitions, result aggregation | +| `metrics.py` | All metric implementations (custom + Azure AI wrappers) | +| `eval_dataset.json` | 30 test cases with ground truth and rubrics | +| `telemetry.py` | Azure Monitor tracing configuration | + +**Generated files** (in `.gitignore`): +| File | Purpose | +|------|---------| +| `evaluation_input_data.jsonl` | Generated during `--remote` evaluation for Foundry upload | +| `eval_results/` | Local evaluation results and reports | + +--- + +## License + +This project is part of the Microsoft OpenAI Workshop. See [LICENSE](../../LICENSE) for details. diff --git a/agentic_ai/evaluations/__init__.py b/agentic_ai/evaluations/__init__.py new file mode 100644 index 000000000..6a3982e3e --- /dev/null +++ b/agentic_ai/evaluations/__init__.py @@ -0,0 +1,29 @@ +""" +AI Agent Evaluation Framework + +Evaluation toolkit for testing AI agents against business scenarios. +""" + +from .evaluator import AgentEvaluationRunner, AgentTrace, TestCaseResult +from .metrics import ( + ToolBehaviorEvaluator, + CompletenessEvaluator, + ResponseQualityEvaluator, + GroundedAccuracyEvaluator, + EvaluationResult, + MetricType +) + +__all__ = [ + "AgentEvaluationRunner", + "AgentTrace", + "TestCaseResult", + "ToolBehaviorEvaluator", + "CompletenessEvaluator", + "ResponseQualityEvaluator", + "GroundedAccuracyEvaluator", + "EvaluationResult", + "MetricType" +] + +__version__ = "1.0.0" diff --git a/agentic_ai/evaluations/eval_dataset.json b/agentic_ai/evaluations/eval_dataset.json new file mode 100644 index 000000000..9f6b2d1cd --- /dev/null +++ b/agentic_ai/evaluations/eval_dataset.json @@ -0,0 +1,602 @@ +{ + "version": "2.0", + "description": "Comprehensive evaluation dataset for Contoso customer support agents", + "metadata": { + "total_scenarios": 30, + "single_turn": 25, + "multi_turn": 5, + "categories": ["billing", "internet", "mobile", "account", "tv", "bundle", "support"] + }, + "test_cases": [ + { + "id": "billing_high_invoice", + "category": "billing", + "name": "Invoice Higher Than Usual", + "customer_query": "Hi, I'm customer 251. I noticed my last invoice was $150, which is much higher than usual. Can you help me understand why?", + "customer_id": 251, + "expected_tools": ["get_customer_detail", "get_billing_summary", "get_subscription_detail", "get_data_usage", "search_knowledge_base"], + "required_tools": ["get_billing_summary", "get_data_usage"], + "success_criteria": { + "must_access_billing": true, + "must_explain_charges": true + }, + "ground_truth_solution": "The customer's invoice is $150 instead of the usual $60 because of data overage charges. Key facts: 1) The plan has a 10GB data cap, 2) Customer used 22GB (12GB over), 3) Overage charges of $7.50/GB apply, 4) Additional $90 explains the higher bill. Solutions: offer courtesy adjustment, recommend plan upgrade, set up usage alerts.", + "scoring_rubric": "5=Identifies overage (22GB vs 10GB), explains charges, offers adjustment AND upgrade; 4=Identifies overage, offers solution; 3=Identifies cause but missing details; 2=Vague explanation; 1=Incorrect or unhelpful", + "multi_turn": false + }, + { + "id": "billing_payment_history", + "category": "billing", + "name": "Payment History Inquiry", + "customer_query": "Hi, I'm customer 5. Can you show me my recent payments? I want to make sure they all went through.", + "customer_id": 5, + "expected_tools": ["get_customer_detail", "get_billing_summary"], + "required_tools": ["get_billing_summary"], + "success_criteria": { + "must_access_billing": true + }, + "ground_truth_solution": "Show payment history with dates, amounts, and methods. Confirm all payments were successful. Mention autopay option if not enabled. Offer to send payment receipts if needed.", + "scoring_rubric": "5=Shows history with dates/amounts/methods, confirms status; 4=Shows history and confirms; 3=Provides payment info but incomplete; 2=Vague response; 1=No payment info", + "multi_turn": false + }, + { + "id": "billing_autopay_setup", + "category": "billing", + "name": "Autopay Setup Request", + "customer_query": "Hi, I'm customer 10. I keep forgetting to pay my bill on time. Can you help me set up autopay?", + "customer_id": 10, + "expected_tools": ["get_customer_detail", "get_billing_summary", "get_subscription_detail", "search_knowledge_base"], + "required_tools": ["get_billing_summary"], + "success_criteria": { + "must_access_billing": true, + "must_explain_autopay": true + }, + "ground_truth_solution": "Check current autopay status. Explain $5 monthly discount for autopay. Guide through setup process. Confirm payment method on file.", + "scoring_rubric": "5=Checks status, mentions $5 discount, explains benefits, guides setup; 4=Explains benefits and setup; 3=Basic autopay info; 2=Generic response; 1=Doesn't help with autopay", + "multi_turn": false + }, + { + "id": "billing_overdue_invoice", + "category": "billing", + "name": "Overdue Invoice Question", + "customer_query": "Hi, I'm customer 15. I received a notice about an overdue invoice. What happens if I don't pay soon?", + "customer_id": 15, + "expected_tools": ["get_customer_detail", "get_billing_summary", "search_knowledge_base"], + "required_tools": ["get_billing_summary"], + "success_criteria": { + "must_access_billing": true, + "must_explain_consequences": true + }, + "ground_truth_solution": "Show overdue invoices with amounts and due dates. Explain late fee policy and potential service suspension after 30+ days. Offer payment plan if large amount. Process payment if customer wants.", + "scoring_rubric": "5=Shows overdue details, explains consequences, offers solutions; 4=Explains consequences and helps; 3=Addresses concern but missing specifics; 2=Generic response; 1=Doesn't address concern", + "multi_turn": false + }, + { + "id": "billing_refund_request", + "category": "billing", + "name": "Refund Request for Service Issue", + "customer_query": "Hi, I'm customer 20. I was without internet for 3 days last week. Can I get a refund or credit for those days?", + "customer_id": 20, + "expected_tools": ["get_customer_detail", "get_support_tickets", "get_subscription_detail", "get_billing_summary"], + "required_tools": ["get_support_tickets", "get_billing_summary"], + "success_criteria": { + "must_verify_outage": true, + "must_offer_credit": true + }, + "ground_truth_solution": "Verify outage via support tickets or service incidents. Calculate pro-rated credit for 3 days. Apply credit to next invoice. Apologize for inconvenience and confirm credit will appear on next bill.", + "scoring_rubric": "5=Verifies outage, calculates credit, applies and confirms; 4=Acknowledges and offers credit; 3=Offers help but missing verification; 2=Generic response; 1=Doesn't address refund", + "multi_turn": false + }, + { + "id": "internet_slow", + "category": "internet", + "name": "Internet Slower Than Before", + "customer_query": "Hi, I'm customer 252. My internet has been really slow lately. I'm paying for 1Gbps but it feels much slower.", + "customer_id": 252, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge_base"], + "required_tools": ["get_subscription_detail"], + "success_criteria": { + "must_check_subscription": true, + "must_provide_troubleshooting": true + }, + "ground_truth_solution": "Check subscription and service status. Look for existing service incidents. Acknowledge known issue if exists. Provide troubleshooting steps (restart router, check cables, test wired). Offer to escalate and mention potential service credit.", + "scoring_rubric": "5=Identifies incident, provides troubleshooting, offers escalation AND credit; 4=Identifies issue and troubleshoots; 3=Acknowledges and provides some steps; 2=Generic troubleshooting; 1=Unhelpful", + "multi_turn": false + }, + { + "id": "internet_upgrade_inquiry", + "category": "internet", + "name": "Internet Speed Upgrade Options", + "customer_query": "Hi, I'm customer 25. I work from home and my current internet is too slow for video calls. What upgrade options do I have?", + "customer_id": 25, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products", "search_knowledge_base"], + "required_tools": ["get_subscription_detail", "get_products"], + "success_criteria": { + "must_check_subscription": true, + "must_show_products": true + }, + "ground_truth_solution": "Check current plan. Show upgrade options: Basic (100Mbps/$49.99), Pro (500Mbps/$79.99), Ultimate (1Gbps/$119.99). Recommend Pro for video calls. Explain benefits, show price difference, mention promotions. Upgrades take effect within 24 hours.", + "scoring_rubric": "5=Shows current plan, presents options with pricing, recommends, mentions promos; 4=Shows options with pricing and recommends; 3=Lists options but missing personalization; 2=Generic product info; 1=No helpful upgrade info", + "multi_turn": false + }, + { + "id": "internet_router_reset", + "category": "internet", + "name": "Router Reset Help", + "customer_query": "Hi, I'm customer 30. My router isn't working and I think I need to reset it. How do I do that?", + "customer_id": 30, + "expected_tools": ["get_customer_detail", "search_knowledge_base"], + "required_tools": ["search_knowledge_base"], + "success_criteria": { + "must_search_knowledge": true, + "must_provide_steps": true + }, + "ground_truth_solution": "Provide step-by-step: 1) Locate reset button on back, 2) Use paperclip to press and hold 10 seconds, 3) Wait for router to restart (lights blink), 4) Returns to factory settings, 5) Reconnect using default WiFi on label. Offer technician if uncomfortable.", + "scoring_rubric": "5=Complete steps, mentions factory settings warning, offers additional help; 4=Provides reset steps and guidance; 3=Gives instructions but incomplete; 2=Vague instructions; 1=Doesn't help", + "multi_turn": false + }, + { + "id": "internet_outage_report", + "category": "internet", + "name": "Internet Outage Report", + "customer_query": "Hi, I'm customer 35. My internet is completely down! Nothing is working. Is there an outage in my area?", + "customer_id": 35, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge_base"], + "required_tools": ["get_subscription_detail"], + "success_criteria": { + "must_check_subscription": true, + "must_check_incidents": true + }, + "ground_truth_solution": "Check subscription service status and existing incidents. If outage confirmed: apologize, provide ETA, offer notification when restored. If no known outage: create support ticket, provide troubleshooting, offer technician visit, mention service credit for extended outages.", + "scoring_rubric": "5=Checks outage status, creates ticket if needed, provides ETA, offers follow-up; 4=Checks status and takes action; 3=Acknowledges and offers help; 2=Generic response; 1=Doesn't address outage", + "multi_turn": false + }, + { + "id": "internet_static_ip", + "category": "internet", + "name": "Static IP Request", + "customer_query": "Hi, I'm customer 40. I need a static IP address for my home server. Do you offer that?", + "customer_id": 40, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"], + "required_tools": ["get_subscription_detail", "get_products"], + "success_criteria": { + "must_check_subscription": true, + "must_explain_options": true + }, + "ground_truth_solution": "Static IP included in: Pro ($79.99/month) - 1 static IP, Ultimate ($119.99/month) - 1 static IP, Business Enterprise ($299.99/month) - IP block. Basic plan does not include. Check current plan and recommend upgrade to Pro if on Basic.", + "scoring_rubric": "5=Checks plan, explains which include static IP, recommends option; 4=Explains availability and recommends upgrade; 3=Mentions static IP but missing plan details; 2=Generic response; 1=Doesn't address request", + "multi_turn": false + }, + { + "id": "roaming_travel", + "category": "mobile", + "name": "Travelling Abroad - Needs Roaming", + "customer_query": "Hi, I'm customer 253. I'm traveling to Spain in 2 days and need to know about international roaming.", + "customer_id": 253, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products", "search_knowledge_base"], + "required_tools": ["get_subscription_detail"], + "success_criteria": { + "must_check_subscription": true, + "must_explain_roaming": true + }, + "ground_truth_solution": "Roaming currently NOT enabled. Packages typically require 3 days to activate (cutting it close). Spain covered under European options. Urgently enable roaming, recommend appropriate package, warn about timeline, explain rates and usage alerts.", + "scoring_rubric": "5=Identifies roaming off, explains urgency, offers to enable AND recommends package; 4=Identifies status and urgency, offers to enable; 3=Identifies not enabled, offers to help; 2=Generic roaming info; 1=Doesn't address request", + "multi_turn": false + }, + { + "id": "mobile_data_usage", + "category": "mobile", + "name": "Mobile Data Usage Check", + "customer_query": "Hi, I'm customer 45. How much data have I used this month? I don't want to go over my limit.", + "customer_id": 45, + "expected_tools": ["get_customer_detail", "get_data_usage", "get_subscription_detail"], + "required_tools": ["get_data_usage", "get_subscription_detail"], + "success_criteria": { + "must_check_usage": true, + "must_show_limit": true + }, + "ground_truth_solution": "Show current data usage for billing cycle, data cap from plan, days remaining, percentage used. If close to limit: warn about overage, suggest data-saving tips, offer unlimited upgrade, explain usage alerts.", + "scoring_rubric": "5=Shows usage, cap, remaining, provides proactive advice; 4=Shows usage and limit clearly; 3=Provides data info; 2=Vague or incomplete; 1=Doesn't provide usage", + "multi_turn": false + }, + { + "id": "mobile_upgrade_premium", + "category": "mobile", + "name": "Mobile Plan Upgrade", + "customer_query": "Hi, I'm customer 3. I keep running out of data. What mobile plans with more data do you have?", + "customer_id": 3, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"], + "required_tools": ["get_subscription_detail", "get_products"], + "success_criteria": { + "must_check_subscription": true, + "must_show_products": true + }, + "ground_truth_solution": "Current plan: Essential (5GB/$29.99). Recommend Premium ($59.99/month): unlimited data, international roaming, 5G Priority, 50GB hotspot. Explain $30/month price difference, highlight unlimited benefit, offer to process upgrade.", + "scoring_rubric": "5=Shows current plan, recommends Premium with pricing, highlights benefits; 4=Provides options with comparison; 3=Mentions options but missing details; 2=Generic product info; 1=Doesn't help", + "multi_turn": false + }, + { + "id": "mobile_hotspot_question", + "category": "mobile", + "name": "Mobile Hotspot Inquiry", + "customer_query": "Hi, I'm customer 8. Does my mobile plan include hotspot? I need to use it for my laptop.", + "customer_id": 8, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"], + "required_tools": ["get_subscription_detail"], + "success_criteria": { + "must_check_subscription": true + }, + "ground_truth_solution": "Check current plan. Essential: Hotspot NOT included or limited. Premium: 50GB hotspot included. Explain availability based on their plan, provide usage info or offer Premium upgrade, give instructions on enabling if available.", + "scoring_rubric": "5=Checks plan, explains status, provides info or upgrade option; 4=Explains availability for their plan; 3=Addresses question generally; 2=Vague without checking plan; 1=Doesn't address hotspot", + "multi_turn": false + }, + { + "id": "account_locked", + "category": "account", + "name": "Account Locked After Failed Logins", + "customer_query": "Hi, I'm customer 254. I can't log into my account - it says it's locked!", + "customer_id": 254, + "expected_tools": ["get_customer_detail", "get_security_logs", "unlock_account", "search_knowledge_base"], + "required_tools": ["get_security_logs", "unlock_account"], + "success_criteria": { + "must_check_security_logs": true, + "must_unlock_account": true + }, + "ground_truth_solution": "Security logs show multiple failed login attempts triggering lockout. Security feature to protect account. Verify identity, unlock account using unlock_account tool, confirm accessible. Suggest password reset, recommend 2FA, advise password manager.", + "scoring_rubric": "5=Verifies identity, unlocks, confirms, provides security recommendations (password, 2FA); 4=Unlocks and provides one recommendation; 3=Unlocks and confirms; 2=Attempts but doesn't unlock; 1=Doesn't address lockout", + "multi_turn": false + }, + { + "id": "account_security_check", + "category": "account", + "name": "Security Audit Request", + "customer_query": "Hi, I'm customer 12. I heard about data breaches in the news. Can you check if my account is secure?", + "customer_id": 12, + "expected_tools": ["get_customer_detail", "get_security_logs", "search_knowledge_base"], + "required_tools": ["get_security_logs"], + "success_criteria": { + "must_check_security_logs": true, + "must_provide_recommendations": true + }, + "ground_truth_solution": "Review security logs for suspicious activity, failed logins from unknown locations, unauthorized access. Provide recommendations: enable 2FA, use strong unique password, update every 90 days, never share credentials, monitor account. Reassure and explain security measures.", + "scoring_rubric": "5=Reviews logs, reports findings, comprehensive recommendations; 4=Checks status and provides recommendations; 3=Reviews but limited recommendations; 2=Generic advice without checking; 1=Doesn't address concern", + "multi_turn": false + }, + { + "id": "account_update_contact", + "category": "account", + "name": "Update Contact Information", + "customer_query": "Hi, I'm customer 18. I have a new email and phone number. Can you update my account information?", + "customer_id": 18, + "expected_tools": ["get_customer_detail"], + "required_tools": ["get_customer_detail"], + "success_criteria": { + "must_access_customer": true + }, + "ground_truth_solution": "Retrieve current contact details. Verify identity. Collect new email and phone. Explain verification process for new contact info. Note: new email may require verification, update affects notifications/billing alerts, password reset links go to email on file.", + "scoring_rubric": "5=Shows current info, requests new details, explains verification, updates preferences; 4=Helps with update and explains process; 3=Acknowledges and provides guidance; 2=Generic without checking info; 1=Doesn't help", + "multi_turn": false + }, + { + "id": "account_paperless_billing", + "category": "account", + "name": "Paperless Billing Setup", + "customer_query": "Hi, I'm customer 22. I want to go paperless and stop receiving paper bills. How do I do that?", + "customer_id": 22, + "expected_tools": ["get_customer_detail", "search_knowledge_base"], + "required_tools": ["get_customer_detail"], + "success_criteria": { + "must_access_customer": true + }, + "ground_truth_solution": "Check current billing preferences. Verify email on file. Enable paperless billing. Confirm: bills sent to email, paper stops within 1-2 cycles, can view all bills online, email notifications for new bills.", + "scoring_rubric": "5=Checks settings, confirms email, enables paperless, explains benefits; 4=Enables and confirms; 3=Provides guidance; 2=Generic without checking; 1=Doesn't help", + "multi_turn": false + }, + { + "id": "tv_channel_lineup", + "category": "tv", + "name": "TV Channel Lineup Question", + "customer_query": "Hi, I'm customer 28. What channels do I get with my TV streaming plan?", + "customer_id": 28, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"], + "required_tools": ["get_subscription_detail"], + "success_criteria": { + "must_check_subscription": true + }, + "ground_truth_solution": "TV plans: Basic ($34.99/month): 50+ channels, 2 screens, 7-day replay. Premium ($64.99/month): 150+ channels, 4 screens, 30-day replay, sports, movies. Check current subscription, list features, mention upgrade if on Basic, explain streaming app access.", + "scoring_rubric": "5=Shows plan details, lists features, mentions upgrade if applicable; 4=Explains channels and features; 3=Provides plan info; 2=Generic TV info; 1=Doesn't address question", + "multi_turn": false + }, + { + "id": "tv_add_sports", + "category": "tv", + "name": "Add Sports Package", + "customer_query": "Hi, I'm customer 32. I want to watch football games. Do you have a sports package I can add?", + "customer_id": 32, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"], + "required_tools": ["get_subscription_detail", "get_products"], + "success_criteria": { + "must_check_subscription": true, + "must_show_products": true + }, + "ground_truth_solution": "Sports included in TV Streaming Premium ($64.99/month). Basic does not include sports. Check current subscription. If on Basic, offer upgrade to Premium which includes sports plus movie channels, 4 screens, 30-day replay. Calculate price difference.", + "scoring_rubric": "5=Checks plan, explains sports in Premium, shows pricing, offers upgrade; 4=Explains availability and upgrade; 3=Mentions sports info; 2=Generic without checking plan; 1=Doesn't help", + "multi_turn": false + }, + { + "id": "bundle_inquiry", + "category": "bundle", + "name": "Bundle Package Inquiry", + "customer_query": "Hi, I'm customer 38. I have internet and mobile separately. Would I save money if I bundle them?", + "customer_id": 38, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products"], + "required_tools": ["get_subscription_detail", "get_products"], + "success_criteria": { + "must_check_subscription": true, + "must_show_products": true + }, + "ground_truth_solution": "Bundle - Family Complete: $199.99/month includes 500Mbps Internet, 150+ TV Channels, 2 Unlimited Mobile Lines, 20% discount vs individual. Check current subscriptions and total cost, calculate potential savings, explain bundle includes more, show value, offer to switch.", + "scoring_rubric": "5=Shows current cost, calculates savings, explains benefits, offers switch; 4=Explains options and potential savings; 3=Provides bundle info; 2=Generic bundle info; 1=Doesn't help", + "multi_turn": false + }, + { + "id": "promotion_eligibility", + "category": "bundle", + "name": "Promotion Eligibility Check", + "customer_query": "Hi, I'm customer 42. Are there any promotions or discounts I'm eligible for?", + "customer_id": 42, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_eligible_promotions"], + "required_tools": ["get_customer_detail", "get_eligible_promotions"], + "success_criteria": { + "must_access_customer": true, + "must_check_promotions": true + }, + "ground_truth_solution": "Available promotions: 1) New Customer 20% off (if new), 2) Bundle & Save $50/month (if 3+ services), 3) Loyalty Reward free speed upgrade (if Gold/Platinum), 4) Refer a Friend $100 credit. Check loyalty level and services, identify applicable promotions, explain how to take advantage.", + "scoring_rubric": "5=Checks eligibility, lists applicable promos, explains how to apply; 4=Identifies promotions customer qualifies for; 3=Mentions available promotions; 2=Generic list without checking; 1=Doesn't help", + "multi_turn": false + }, + { + "id": "loyalty_benefits", + "category": "bundle", + "name": "Loyalty Program Benefits", + "customer_query": "Hi, I'm customer 48. I've been with you for years. What loyalty benefits do I get?", + "customer_id": 48, + "expected_tools": ["get_customer_detail", "get_products", "search_knowledge_base"], + "required_tools": ["get_customer_detail"], + "success_criteria": { + "must_access_customer": true + }, + "ground_truth_solution": "Loyalty tiers: Bronze (basic support), Silver (priority support, occasional discounts), Gold (24/7 VIP support, free speed upgrades, special promotions), Platinum (all Gold plus dedicated account manager). Check current level, explain tier benefits, mention how to reach next tier, highlight current Gold/Platinum promotion.", + "scoring_rubric": "5=Shows level, explains tier benefits, mentions upgrade path and promos; 4=Explains benefits for their tier; 3=Provides loyalty info; 2=Generic without checking level; 1=Doesn't address question", + "multi_turn": false + }, + { + "id": "support_ticket_status", + "category": "support", + "name": "Support Ticket Status Check", + "customer_query": "Hi, I'm customer 6. I opened a support ticket a few days ago. Can you check the status?", + "customer_id": 6, + "expected_tools": ["get_customer_detail", "get_support_tickets"], + "required_tools": ["get_support_tickets"], + "success_criteria": { + "must_check_tickets": true + }, + "ground_truth_solution": "Look up open/pending tickets. Provide ticket number and status. Explain current stage of resolution. Provide expected timeline. If pending: explain what's being done, offer to escalate if delayed, provide contact for urgent issues.", + "scoring_rubric": "5=Finds ticket, shows status, explains next steps, offers escalation; 4=Provides status and explanation; 3=Finds and reports status; 2=Generic without checking; 1=Doesn't help", + "multi_turn": false + }, + { + "id": "support_new_ticket", + "category": "support", + "name": "Create New Support Ticket", + "customer_query": "Hi, I'm customer 14. My cable box keeps rebooting randomly. I need someone to look at this.", + "customer_id": 14, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_support_tickets"], + "required_tools": ["get_subscription_detail"], + "success_criteria": { + "must_check_subscription": true, + "must_offer_support": true + }, + "ground_truth_solution": "Document cable box issue (random reboots). Check subscription for equipment details. Basic troubleshooting: unplug 30 seconds, check connections. If persists, create support ticket with equipment type, issue description, troubleshooting attempted, priority level. Offer technician visit.", + "scoring_rubric": "5=Documents issue, tries troubleshooting, creates ticket, offers technician; 4=Creates ticket and offers options; 3=Acknowledges and offers help; 2=Generic troubleshooting without ticket; 1=Doesn't address issue", + "multi_turn": false + }, + { + "id": "multi_billing_dispute", + "category": "billing", + "name": "[Multi-Turn] Billing Dispute Resolution", + "multi_turn": true, + "turns": [ + { + "turn_number": 1, + "customer_query": "Hi, I'm customer 7. There's a $50 charge on my bill I don't recognize. What is this for?", + "expected_tools": ["get_billing_summary"], + "expected_response_elements": ["charge", "invoice", "billing"] + }, + { + "turn_number": 2, + "customer_query": "I didn't order any equipment or additional services. Can you remove this charge?", + "expected_tools": [], + "expected_response_elements": ["credit", "remove", "adjustment"] + }, + { + "turn_number": 3, + "customer_query": "Thanks for the credit. While I have you, are there any promotions I qualify for?", + "expected_tools": ["get_customer_detail", "get_eligible_promotions"], + "expected_response_elements": ["promotion", "discount", "offer"] + } + ], + "customer_id": 7, + "expected_tools": ["get_customer_detail", "get_billing_summary", "get_subscription_detail", "get_eligible_promotions"], + "required_tools": ["get_billing_summary"], + "success_criteria": { + "must_access_billing": true, + "must_handle_credit": true, + "must_check_promotions": true + }, + "ground_truth_solution": "Turn 1: Pull billing summary, identify $50 charge, explain what it's for. Turn 2: If erroneous, apply credit; if valid, explain but offer goodwill credit if appropriate, confirm adjustment on next bill. Turn 3: Review loyalty level and services, identify applicable promotions, recommend best options.", + "scoring_rubric": "5=Investigates thoroughly, handles credit appropriately, provides personalized promo info; 4=Addresses each turn adequately; 3=Responds but missing depth; 2=Misses context between turns; 1=Fails to address dispute or loses context" + }, + { + "id": "multi_internet_troubleshoot", + "category": "internet", + "name": "[Multi-Turn] Internet Troubleshooting Flow", + "multi_turn": true, + "turns": [ + { + "turn_number": 1, + "customer_query": "Hi, I'm customer 16. My internet keeps dropping every few minutes. It's really frustrating.", + "expected_tools": ["get_subscription_detail", "get_support_tickets"], + "expected_response_elements": ["internet", "issue", "connection"] + }, + { + "turn_number": 2, + "customer_query": "I already tried restarting the router. It worked for a bit but started dropping again.", + "expected_tools": ["search_knowledge_base"], + "expected_response_elements": ["troubleshoot", "check", "cable"] + }, + { + "turn_number": 3, + "customer_query": "I checked the cables and they look fine. I think there might be something wrong with the equipment.", + "expected_tools": [], + "expected_response_elements": ["technician", "appointment", "visit"] + }, + { + "turn_number": 4, + "customer_query": "Yes, please schedule a technician. What times are available?", + "expected_tools": [], + "expected_response_elements": ["scheduled", "appointment", "confirm"] + } + ], + "customer_id": 16, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge_base"], + "required_tools": ["get_subscription_detail"], + "success_criteria": { + "must_check_subscription": true, + "must_provide_troubleshooting": true, + "must_offer_technician": true + }, + "ground_truth_solution": "Turn 1: Check subscription/incidents, acknowledge issue. Turn 2: Since router restart tried, suggest cable check, wired connection test, interference check. Turn 3: Acknowledge customer tried troubleshooting, agree equipment may need inspection, offer technician. Turn 4: Offer time slots, confirm details, provide arrival window.", + "scoring_rubric": "5=Progressive troubleshooting, builds on previous turns, smooth escalation; 4=Addresses each step, schedules technician; 3=Follows flow but may skip steps; 2=Repetitive or doesn't build on attempts; 1=Doesn't progress logically" + }, + { + "id": "multi_service_cancellation", + "category": "account", + "name": "[Multi-Turn] Service Cancellation Retention", + "multi_turn": true, + "turns": [ + { + "turn_number": 1, + "customer_query": "Hi, I'm customer 24. I want to cancel my internet service. It's too expensive.", + "expected_tools": ["get_subscription_detail", "get_billing_summary"], + "expected_response_elements": ["cancel", "service", "understand"] + }, + { + "turn_number": 2, + "customer_query": "I've been paying $119 a month and I found a competitor offering $70 for similar speeds.", + "expected_tools": ["get_products"], + "expected_response_elements": ["offer", "discount", "match", "retention"] + }, + { + "turn_number": 3, + "customer_query": "A 20% discount sounds good. What would my new monthly rate be?", + "expected_tools": [], + "expected_response_elements": ["$95", "monthly", "rate", "discount"] + } + ], + "customer_id": 24, + "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_billing_summary", "get_products"], + "required_tools": ["get_subscription_detail", "get_billing_summary"], + "success_criteria": { + "must_check_subscription": true, + "must_attempt_retention": true + }, + "ground_truth_solution": "Turn 1: Pull subscription/billing, express understanding, ask about needs, don't immediately accept cancellation. Turn 2: Acknowledge competitor pricing, check retention offers, offer 20% loyalty discount or price match, highlight value-adds. Turn 3: Calculate new rate ($119 x 0.8 = $95.20), confirm discount applied, explain duration, thank for staying.", + "scoring_rubric": "5=Empathetic handling, competitive retention offer, calculates rate, secures retention; 4=Makes appropriate offer and calculates; 3=Attempts retention but may miss personalization; 2=Too quick to cancel or weak retention; 1=Processes cancellation without effort" + }, + { + "id": "multi_new_customer_setup", + "category": "internet", + "name": "[Multi-Turn] New Service Setup Assistance", + "multi_turn": true, + "turns": [ + { + "turn_number": 1, + "customer_query": "Hi, I'm customer 2. I just moved to a new apartment and need to set up internet. What are my options?", + "expected_tools": ["get_products"], + "expected_response_elements": ["internet", "plans", "options"] + }, + { + "turn_number": 2, + "customer_query": "I work from home and need reliable internet for video calls. Which plan do you recommend?", + "expected_tools": ["get_subscription_detail"], + "expected_response_elements": ["Pro", "500Mbps", "recommend"] + }, + { + "turn_number": 3, + "customer_query": "The Pro plan sounds good. Do you have any current promotions for new setups?", + "expected_tools": ["get_eligible_promotions"], + "expected_response_elements": ["promotion", "discount", "new customer"] + }, + { + "turn_number": 4, + "customer_query": "Great! Please set me up with the Pro plan and the new customer discount.", + "expected_tools": [], + "expected_response_elements": ["confirm", "order", "setup", "welcome"] + } + ], + "customer_id": 2, + "expected_tools": ["get_customer_detail", "get_products", "get_subscription_detail", "get_eligible_promotions"], + "required_tools": ["get_products"], + "success_criteria": { + "must_show_products": true, + "must_recommend_plan": true, + "must_check_promotions": true + }, + "ground_truth_solution": "Turn 1: List plans (Basic, Pro, Ultimate) with speeds and pricing. Turn 2: Recommend Pro (500Mbps) for WFH video calls, explain why suitable. Turn 3: New Customer 20% off first 3 months, WiFi 6 router included, installation options. Turn 4: Confirm Pro @ $79.99, apply 20% (first 3 months = $63.99), set installation, welcome.", + "scoring_rubric": "5=Natural sales flow, personalized recommendation, applies promo, completes smoothly; 4=Guides through selection and setup; 3=Completes but may lack personalization; 2=Disjointed or missing steps; 1=Doesn't complete setup" + }, + { + "id": "multi_complex_account_issue", + "category": "account", + "name": "[Multi-Turn] Complex Account Resolution", + "multi_turn": true, + "turns": [ + { + "turn_number": 1, + "customer_query": "Hi, I'm customer 11. I have several issues. First, I was charged for a service I cancelled last month.", + "expected_tools": ["get_billing_summary", "get_subscription_detail"], + "expected_response_elements": ["charge", "cancelled", "billing"] + }, + { + "turn_number": 2, + "customer_query": "Also, my internet has been slow for the past week. Are there any known issues?", + "expected_tools": ["get_support_tickets"], + "expected_response_elements": ["slow", "internet", "incident", "issue"] + }, + { + "turn_number": 3, + "customer_query": "One more thing - I want to downgrade my TV package. I don't watch that much anymore.", + "expected_tools": ["get_products"], + "expected_response_elements": ["downgrade", "TV", "package", "change"] + }, + { + "turn_number": 4, + "customer_query": "Can you summarize all the changes you're making to my account?", + "expected_tools": [], + "expected_response_elements": ["summary", "credit", "downgrade", "changes"] + } + ], + "customer_id": 11, + "expected_tools": ["get_customer_detail", "get_billing_summary", "get_subscription_detail", "get_support_tickets", "get_products"], + "required_tools": ["get_billing_summary", "get_subscription_detail"], + "success_criteria": { + "must_access_billing": true, + "must_check_subscription": true, + "must_handle_multiple_issues": true + }, + "ground_truth_solution": "Turn 1: Check billing for cancelled service charge, identify erroneous charge, apply credit/refund. Turn 2: Check service incidents and subscription status, provide status/ETA or troubleshooting. Turn 3: Show current TV package, explain downgrade options (Premium to Basic), calculate savings, process change. Turn 4: Recap all changes: credit applied, internet issue status, TV downgrade and savings.", + "scoring_rubric": "5=Handles all 3 issues effectively, provides clear summary, maintains context; 4=Addresses all with reasonable resolution; 3=Handles most but may miss one or lack summary; 2=Loses track or incomplete resolution; 1=Unable to handle multiple issues" + } + ] +} diff --git a/agentic_ai/evaluations/evaluator.py b/agentic_ai/evaluations/evaluator.py new file mode 100644 index 000000000..c0028f1bf --- /dev/null +++ b/agentic_ai/evaluations/evaluator.py @@ -0,0 +1,458 @@ +""" +Evaluation runner for AI Agent testing. +Tests agents against the evaluation dataset and generates reports. +Supports multi-turn conversations and Azure AI Foundry evaluators. +""" + +import json +import os +from typing import Dict, List, Any, Optional +from datetime import datetime +from dataclasses import dataclass, asdict, field +import sys + +from metrics import ( + ToolBehaviorEvaluator, + CompletenessEvaluator, + ResponseQualityEvaluator, + GroundedAccuracyEvaluator, + AzureAIEvaluatorSuite, + EvaluationResult, + AZURE_EVALUATORS_AVAILABLE, +) + + +@dataclass +class AgentTrace: + """Captured trace of agent execution.""" + query: str + response: str + tool_calls: List[Dict[str, Any]] + metadata: Dict[str, Any] + + +@dataclass +class ConversationTurn: + """A single turn in a multi-turn conversation.""" + query: str + response: str + tool_calls: List[Dict[str, Any]] = field(default_factory=list) + + +@dataclass +class MultiTurnTrace: + """Captured trace of a multi-turn conversation.""" + turns: List[ConversationTurn] + metadata: Dict[str, Any] + + @property + def full_response(self) -> str: + """Concatenate all responses for evaluation.""" + return "\n\n".join(t.response for t in self.turns) + + @property + def all_tool_calls(self) -> List[Dict[str, Any]]: + """Aggregate all tool calls across turns.""" + return [call for turn in self.turns for call in turn.tool_calls] + + @property + def first_query(self) -> str: + """Get the first query for matching.""" + return self.turns[0].query if self.turns else "" + + +@dataclass +class TestCaseResult: + """Result of evaluating a single test case.""" + test_case_id: str + query: str + agent_response: str + metrics: List[EvaluationResult] + overall_score: float + passed: bool + timestamp: str + is_multi_turn: bool = False + turn_count: int = 1 + + +class AgentEvaluationRunner: + """Main evaluation runner for agent testing.""" + + # Weights for SINGLE-TURN evaluation (tool-focused) + SINGLE_TURN_WEIGHTS = { + "tool_behavior": 0.10, + "tool_call_accuracy": 0.15, + "task_adherence": 0.10, + "completeness": 0.10, + "response_quality_llm": 0.10, + "response_quality_basic": 0.05, + "grounded_accuracy": 0.10, + "intent_resolution": 0.10, + "coherence": 0.05, + "fluency": 0.05, + "relevance": 0.05, + "solution_accuracy": 0.10, + } + + # Weights for MULTI-TURN evaluation (outcome-focused) + # De-emphasizes tool-level metrics, focuses on overall outcome + MULTI_TURN_WEIGHTS = { + "solution_accuracy": 0.30, # Did we achieve the right outcome? + "task_adherence": 0.20, # Did we follow proper procedures? + "intent_resolution": 0.20, # Were all intents resolved? + "coherence": 0.10, # Was conversation logical? + "fluency": 0.10, # Was communication quality good? + "relevance": 0.10, # Were responses relevant throughout? + # Tool metrics excluded from multi-turn + } + + def __init__( + self, + dataset_path: str = "eval_dataset.json", + azure_openai_client=None, + use_azure_evaluators: bool = True, + ): + """ + Initialize evaluation runner. + + Args: + dataset_path: Path to evaluation dataset JSON + azure_openai_client: Optional Azure OpenAI client for LLM-as-judge + use_azure_evaluators: Whether to use Azure AI Foundry evaluators + """ + self.dataset_path = dataset_path + self.test_cases = self._load_dataset() + self.llm_client = azure_openai_client + + # Initialize evaluators + self.tool_evaluator = ToolBehaviorEvaluator() + self.completeness_evaluator = CompletenessEvaluator() + self.quality_evaluator = ResponseQualityEvaluator(azure_openai_client) + self.accuracy_evaluator = GroundedAccuracyEvaluator(azure_openai_client) + + # Initialize Azure AI evaluators if available and enabled + self.azure_evaluators = None + if use_azure_evaluators and AZURE_EVALUATORS_AVAILABLE: + self.azure_evaluators = AzureAIEvaluatorSuite() + if not self.azure_evaluators.available: + self.azure_evaluators = None + + def _load_dataset(self) -> List[Dict[str, Any]]: + """Load evaluation dataset from JSON.""" + with open(self.dataset_path, 'r', encoding='utf-8') as f: + data = json.load(f) + return data.get("test_cases", []) + + def evaluate_agent_response( + self, + test_case: Dict[str, Any], + agent_trace: AgentTrace, + ) -> TestCaseResult: + """ + Evaluate a single agent response against test case. + + Args: + test_case: Test case from dataset + agent_trace: Captured agent execution trace + + Returns: + TestCaseResult with all evaluation metrics + """ + metrics: List[EvaluationResult] = [] + is_multi_turn = test_case.get("multi_turn", False) + + # 1. Evaluate tool usage + tool_names = [call.get("name", "") for call in agent_trace.tool_calls] + + # Use required_tools if specified, otherwise fall back to expected_tools + required_tools = test_case.get("required_tools") + if required_tools is None: + required_tools = test_case.get("expected_tools", []) + + tool_result = self.tool_evaluator.evaluate( + expected_tools=test_case.get("expected_tools", []), + actual_tools=tool_names, + required_tools=required_tools + ) + metrics.append(tool_result) + + # 2. Evaluate completeness + completeness_result = self.completeness_evaluator.evaluate( + success_criteria=test_case.get("success_criteria", {}), + tool_calls=agent_trace.tool_calls + ) + metrics.append(completeness_result) + + # 3. Evaluate response quality + tool_summary = f"Tools used: {', '.join(tool_names)}" if tool_names else "No tools used" + quality_result = self.quality_evaluator.evaluate( + query=agent_trace.query, + response=agent_trace.response, + tool_summary=tool_summary + ) + metrics.append(quality_result) + + # 4. Evaluate accuracy (if ground truth available) + tool_outputs = "; ".join([str(call.get("result", "")) for call in agent_trace.tool_calls if call.get("result")]) + accuracy_result = self.accuracy_evaluator.evaluate( + response=agent_trace.response, + tool_outputs=tool_outputs if tool_outputs else None + ) + metrics.append(accuracy_result) + + # 5. Azure AI Foundry evaluators (if available) + if self.azure_evaluators: + azure_results = self.azure_evaluators.evaluate_all( + query=agent_trace.query, + response=agent_trace.response, + ground_truth=test_case.get("ground_truth_solution"), + scoring_rubric=test_case.get("scoring_rubric"), + tool_calls=agent_trace.tool_calls if not is_multi_turn else None, # Skip tool eval for multi-turn + llm_client=self.llm_client, + ) + metrics.extend(azure_results) + + # Use different weights based on single-turn vs multi-turn + if is_multi_turn: + weights = self.MULTI_TURN_WEIGHTS + # For multi-turn, only require outcome metrics to pass + required_pass_metrics = [] # No strict requirements, use overall score + else: + weights = self.SINGLE_TURN_WEIGHTS + required_pass_metrics = ["tool_behavior", "completeness"] + + total_score = 0.0 + total_weight = 0.0 + + for metric in metrics: + weight = weights.get(metric.metric_name, 0.0) # 0 weight = excluded + if weight > 0: + total_score += metric.score * weight + total_weight += weight + + # Overall score is weighted average (on 1-5 scale) + overall_score = total_score / total_weight if total_weight > 0 else 0.0 + # Threshold: 3/5 to pass + if is_multi_turn: + passed = overall_score >= 3.0 # Outcome-based pass for multi-turn + else: + passed = overall_score >= 3.0 and all(m.passed for m in metrics if m.metric_name in required_pass_metrics) + + return TestCaseResult( + test_case_id=test_case.get("id", "unknown"), + query=agent_trace.query, + agent_response=agent_trace.response, + metrics=metrics, + overall_score=overall_score, + passed=passed, + timestamp=datetime.now().isoformat(), + is_multi_turn=is_multi_turn, + turn_count=len(test_case.get("turns", [])) if is_multi_turn else 1, + ) + + def run_evaluation( + self, + agent_traces: List[AgentTrace], + output_dir: str = "eval_results" + ) -> Dict[str, Any]: + """ + Run evaluation on all agent traces. + + Args: + agent_traces: List of captured agent execution traces + output_dir: Directory to save evaluation results + + Returns: + Summary of evaluation results + """ + os.makedirs(output_dir, exist_ok=True) + + results: List[TestCaseResult] = [] + + # Match traces to test cases + for test_case in self.test_cases: + # Find matching trace by test_id in metadata or by query + matching_trace = None + test_id = test_case.get("id", "") + + # Get customer query - for multi-turn, use first turn's query + if test_case.get("multi_turn", False): + turns = test_case.get("turns", []) + customer_query = turns[0]["customer_query"] if turns else "" + else: + customer_query = test_case.get("customer_query", "") + + for trace in agent_traces: + # Match by test_id in metadata first + if trace.metadata.get("test_id") == test_id: + matching_trace = trace + break + # Fallback to query matching + if customer_query and trace.query.lower().strip() == customer_query.lower().strip(): + matching_trace = trace + break + + if not matching_trace: + print(f"⚠ Warning: No trace found for test case {test_case['id']}") + continue + + # Evaluate + result = self.evaluate_agent_response(test_case, matching_trace) + results.append(result) + + # Print progress + status = "✓ PASS" if result.passed else "✗ FAIL" + print(f"{status} {result.test_case_id}: {result.overall_score:.2f}") + + # Generate summary + summary = self._generate_summary(results) + + # Save results + self._save_results(results, summary, output_dir) + + return summary + + def _generate_summary(self, results: List[TestCaseResult]) -> Dict[str, Any]: + """Generate summary statistics.""" + total = len(results) + passed = sum(1 for r in results if r.passed) + + avg_score = sum(r.overall_score for r in results) / total if total > 0 else 0.0 + + # Metric breakdowns + metric_scores = {} + for result in results: + for metric in result.metrics: + if metric.metric_name not in metric_scores: + metric_scores[metric.metric_name] = [] + metric_scores[metric.metric_name].append(metric.score) + + metric_averages = { + name: sum(scores) / len(scores) if scores else 0.0 + for name, scores in metric_scores.items() + } + + return { + "timestamp": datetime.now().isoformat(), + "total_tests": total, + "passed": passed, + "failed": total - passed, + "pass_rate": passed / total if total > 0 else 0.0, + "average_score": avg_score, + "metric_averages": metric_averages + } + + def _save_results( + self, + results: List[TestCaseResult], + summary: Dict[str, Any], + output_dir: str + ): + """Save evaluation results to files.""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + # Save detailed results + results_file = os.path.join(output_dir, f"eval_results_{timestamp}.json") + with open(results_file, 'w') as f: + json.dump({ + "results": [self._result_to_dict(r) for r in results], + "summary": summary + }, f, indent=2) + + # Save summary report + report_file = os.path.join(output_dir, f"eval_report_{timestamp}.txt") + with open(report_file, 'w', encoding='utf-8') as f: + f.write(self._generate_text_report(results, summary)) + + print(f"\n✓ Results saved to: {results_file}") + print(f"✓ Report saved to: {report_file}") + + def _result_to_dict(self, result: TestCaseResult) -> Dict[str, Any]: + """Convert TestCaseResult to dictionary.""" + return { + "test_case_id": result.test_case_id, + "query": result.query, + "agent_response": result.agent_response, + "overall_score": result.overall_score, + "passed": result.passed, + "timestamp": result.timestamp, + "metrics": [ + { + "name": m.metric_name, + "type": m.metric_type.value, + "score": m.score, + "passed": m.passed, + "explanation": m.explanation, + "details": m.details + } + for m in result.metrics + ] + } + + def _generate_text_report( + self, + results: List[TestCaseResult], + summary: Dict[str, Any] + ) -> str: + """Generate human-readable text report.""" + lines = [] + lines.append("=" * 80) + lines.append("AI AGENT EVALUATION REPORT") + lines.append("=" * 80) + lines.append(f"\nTimestamp: {summary['timestamp']}") + lines.append(f"Total Tests: {summary['total_tests']}") + lines.append(f"Passed: {summary['passed']}") + lines.append(f"Failed: {summary['failed']}") + lines.append(f"Pass Rate: {summary['pass_rate']:.1%}") + lines.append(f"Average Score: {summary['average_score']:.2f}") + + lines.append("\n" + "=" * 80) + lines.append("METRIC AVERAGES") + lines.append("=" * 80) + for metric, avg in summary['metric_averages'].items(): + lines.append(f"{metric:30s}: {avg:.2f}") + + lines.append("\n" + "=" * 80) + lines.append("DETAILED RESULTS") + lines.append("=" * 80) + + for result in results: + status = "✓ PASS" if result.passed else "✗ FAIL" + lines.append(f"\n{status} {result.test_case_id} (Score: {result.overall_score:.2f})") + lines.append(f"Query: {result.query}") + lines.append("\nMetrics:") + for metric in result.metrics: + lines.append(f" - {metric.metric_name}: {metric.score:.2f} - {metric.explanation}") + + return "\n".join(lines) + + +def example_usage(): + """Example of how to use the evaluation runner.""" + + # This is an example - in practice, you'd capture real agent traces + example_traces = [ + AgentTrace( + query="I noticed my last invoice was higher than usual—can you help me understand why and what can be done about it?", + response="I've checked your billing history and found that your invoice increased due to a plan upgrade last month. According to our billing policy, you can request a review within 30 days.", + tool_calls=[ + {"name": "get_customer_detail", "args": {"customer_id": 1001}, "result": {}}, + {"name": "get_billing_summary", "args": {"customer_id": 1001}, "result": {}}, + {"name": "search_knowledge_base", "args": {"query": "billing policy"}, "result": {}} + ], + metadata={"agent_type": "single_agent", "duration_ms": 2500} + ) + ] + + # Run evaluation + runner = AgentEvaluationRunner(dataset_path="eval_dataset.json") + summary = runner.run_evaluation(example_traces, output_dir="eval_results") + + print("\n" + "=" * 80) + print("EVALUATION SUMMARY") + print("=" * 80) + print(json.dumps(summary, indent=2)) + + +if __name__ == "__main__": + example_usage() diff --git a/agentic_ai/evaluations/metrics.py b/agentic_ai/evaluations/metrics.py new file mode 100644 index 000000000..2820ed96d --- /dev/null +++ b/agentic_ai/evaluations/metrics.py @@ -0,0 +1,1106 @@ +""" +Evaluation metrics for AI Agent performance assessment. +Pattern-agnostic metrics that work across: +- single agents +- handoff agents +- reflection agents +- research/magentic agents + +Includes Azure AI Foundry evaluators for LLM-as-judge evaluation. +""" + +import os +from typing import Dict, List, Any, Optional +from dataclasses import dataclass +from enum import Enum + +# Azure AI Foundry Evaluators (optional - graceful degradation if not available) +try: + from azure.ai.evaluation import ( + IntentResolutionEvaluator, + TaskAdherenceEvaluator, + ToolCallAccuracyEvaluator, + CoherenceEvaluator, + FluencyEvaluator, + RelevanceEvaluator, + ) + AZURE_EVALUATORS_AVAILABLE = True +except ImportError: + AZURE_EVALUATORS_AVAILABLE = False + IntentResolutionEvaluator = None + TaskAdherenceEvaluator = None + ToolCallAccuracyEvaluator = None + CoherenceEvaluator = None + FluencyEvaluator = None + RelevanceEvaluator = None + + +# ========================= +# Metric Types +# ========================= + +class MetricType(Enum): + TOOL_BEHAVIOR = "tool_behavior" + RESPONSE_QUALITY = "response_quality" + ACCURACY = "accuracy" + COMPLETENESS = "completeness" + EFFICIENCY = "efficiency" + SAFETY = "safety" + INTENT = "intent" + COHERENCE = "coherence" + FLUENCY = "fluency" + RELEVANCE = "relevance" + SOLUTION_ACCURACY = "solution_accuracy" + TASK_COMPLETION = "task_completion" + + +# ========================= +# Result Container +# ========================= + +@dataclass +class EvaluationResult: + metric_name: str + metric_type: MetricType + score: float # 1.0 – 5.0 scale (matching Foundry portal) + passed: bool + details: Dict[str, Any] + explanation: str + + +# ========================= +# Tool Behavior Evaluator (Upgraded) +# ========================= + +class ToolBehaviorEvaluator: + """ + Pattern-agnostic tool scoring: + - recall (required tools used) + - precision (relevant vs total) + - efficiency (minimal sufficiency) + """ + + def evaluate( + self, + expected_tools: List[str], + actual_tools: List[str], + required_tools: Optional[List[str]] = None, + ) -> EvaluationResult: + + required_tools = required_tools or expected_tools + + actual_set = set(actual_tools) + expected_set = set(expected_tools) + required_set = set(required_tools) + + required_hit = required_set & actual_set + missing_required = required_set - actual_set + extra_tools = actual_set - expected_set + relevant_used = actual_set & expected_set + + # --- Scores --- + + recall = len(required_hit) / len(required_set) if required_set else 1.0 + precision = len(relevant_used) / len(actual_set) if actual_set else 1.0 + efficiency = len(required_set) / len(actual_set) if actual_set else 1.0 + efficiency = min(efficiency, 1.0) + + # Combined ratio (0-1), then scale to 1-5 + combined = (recall * 0.5) + (precision * 0.3) + (efficiency * 0.2) + score = 1.0 + (combined * 4.0) # Maps 0-1 to 1-5 scale + + passed = recall == 1.0 # All required tools must be used + + details = { + "recall": recall, + "precision": precision, + "efficiency": efficiency, + "missing_required": list(missing_required), + "extra_tools": list(extra_tools), + "required_hit": list(required_hit), + } + + explanation = ( + f"Recall={recall:.2f} Precision={precision:.2f} " + f"Efficiency={efficiency:.2f} Score={score:.1f}/5" + ) + + return EvaluationResult( + metric_name="tool_behavior", + metric_type=MetricType.TOOL_BEHAVIOR, + score=score, + passed=passed, + details=details, + explanation=explanation, + ) + + +# ========================= +# Completeness Evaluator (Hybrid) +# ========================= + +class CompletenessEvaluator: + """ + Deterministic tool checks + optional LLM semantic checks. + """ + + TOOL_CRITERIA_MAP = { + "must_access_billing": ["get_billing_summary", "get_subscription_detail"], + "must_check_subscription": ["get_subscription_detail"], + "must_check_security_logs": ["get_security_logs"], + "must_check_promotions": ["get_eligible_promotions"], + "must_check_orders": ["get_customer_orders"], + } + + def evaluate( + self, + success_criteria: Dict[str, bool], + tool_calls: List[Dict[str, Any]], + ) -> EvaluationResult: + + tool_names = [c.get("name", "") for c in tool_calls] + results = {} + + for criterion, required in success_criteria.items(): + + if not required: + results[criterion] = True + continue + + if criterion in self.TOOL_CRITERIA_MAP: + needed = self.TOOL_CRITERIA_MAP[criterion] + results[criterion] = any(t in tool_names for t in needed) + else: + # semantic criteria handled by LLM judge metric + results[criterion] = True + + required_count = sum(success_criteria.values()) + met_count = sum( + 1 for k, v in results.items() + if v and success_criteria.get(k) + ) + + # Scale to 1-5 (0 if no requirements, otherwise proportional) + ratio = met_count / required_count if required_count else 1.0 + score = 1.0 + (ratio * 4.0) # Maps 0-1 ratio to 1-5 scale + passed = met_count == required_count # All requirements must be met + + return EvaluationResult( + metric_name="completeness", + metric_type=MetricType.COMPLETENESS, + score=score, + passed=passed, + details=results, + explanation=f"{met_count}/{required_count} required criteria met", + ) + + +# ========================= +# Efficiency Evaluator (NEW) +# ========================= + +class EfficiencyEvaluator: + """ + Pattern-agnostic step efficiency metric. + """ + + def evaluate( + self, + actual_tool_calls: int, + required_tools: int, + ) -> EvaluationResult: + + baseline = max(required_tools, 1) + efficiency = baseline / max(actual_tool_calls, 1) + efficiency = min(efficiency, 1.0) + + # Scale to 1-5 + score = 1.0 + (efficiency * 4.0) + + return EvaluationResult( + metric_name="step_efficiency", + metric_type=MetricType.EFFICIENCY, + score=score, + passed=score >= 3.0, # Threshold: 3/5 + details={ + "actual_calls": actual_tool_calls, + "baseline_required": baseline, + }, + explanation=f"Efficiency {score:.1f}/5", + ) + + +# ========================= +# LLM Judge Evaluator (Upgraded) +# ========================= + +class ResponseQualityEvaluator: + + def __init__(self, llm_client=None): + self.client = llm_client + + def evaluate( + self, + query: str, + response: str, + tool_summary: Optional[str] = None, + ) -> EvaluationResult: + + if not self.client: + return self._basic(response) + + prompt = f""" +Evaluate this customer support response. + +Query: {query} +Response: {response} +Tool Evidence: {tool_summary} + +Score 0–10: +- relevance +- clarity +- completeness +- professionalism +- actionability +- groundedness (uses evidence, not guesses) +- safety (no over-promising) + +Return JSON with overall_score and explanation. +""" + + try: + r = self.client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": "Expert evaluator."}, + {"role": "user", "content": prompt}, + ], + response_format={"type": "json_object"}, + ) + + import json + data = json.loads(r.choices[0].message.content) + + # Convert 0-10 to 1-5 scale + raw_score = data["overall_score"] + score = 1.0 + (raw_score / 10.0) * 4.0 # Maps 0-10 to 1-5 + + return EvaluationResult( + metric_name="response_quality", + metric_type=MetricType.RESPONSE_QUALITY, + score=score, + passed=score >= 3.0, # Threshold: 3/5 + details=data, + explanation=data.get("explanation", ""), + ) + + except Exception: + return self._basic(response) + + def _basic(self, response: str) -> EvaluationResult: + ok = len(response.split()) > 15 + score = 5.0 if ok else 1.0 # 5/5 for good, 1/5 for bad + return EvaluationResult( + metric_name="response_quality_basic", + metric_type=MetricType.RESPONSE_QUALITY, + score=score, + passed=ok, + details={}, + explanation="Basic length check", + ) + + +# ========================= +# Grounded Accuracy Evaluator (NEW) +# ========================= + +class GroundedAccuracyEvaluator: + """ + Checks if response contradicts tool outputs (LLM-assisted). + """ + + def __init__(self, llm_client=None): + self.client = llm_client + + def evaluate( + self, + response: str, + tool_outputs: Optional[str], + ) -> EvaluationResult: + + if not self.client or not tool_outputs: + return EvaluationResult( + metric_name="grounded_accuracy", + metric_type=MetricType.ACCURACY, + score=5.0, # Default pass on 1-5 scale + passed=True, + details={}, + explanation="No grounding check available", + ) + + prompt = f""" +Tool facts: +{tool_outputs} + +Response: +{response} + +Does the response contradict the tool facts? +Answer JSON: {{ "contradiction": true/false }} +""" + + try: + r = self.client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + response_format={"type": "json_object"}, + ) + + import json + data = json.loads(r.choices[0].message.content) + contradiction = data.get("contradiction", False) + + # 1-5 scale: 5 for grounded, 1 for contradiction + score = 1.0 if contradiction else 5.0 + + return EvaluationResult( + metric_name="grounded_accuracy", + metric_type=MetricType.ACCURACY, + score=score, + passed=not contradiction, + details=data, + explanation="Contradiction detected" if contradiction else "Grounded", + ) + + except Exception: + return EvaluationResult( + metric_name="grounded_accuracy", + metric_type=MetricType.ACCURACY, + score=5.0, # Default pass on 1-5 scale + passed=True, + details={}, + explanation="Grounding check failed → default pass", + ) + + +# ========================= +# Safety / Overreach Evaluator (NEW) +# ========================= + +class SafetyEvaluator: + + RISKY_PATTERNS = [ + "guarantee refund", + "will definitely refund", + "account unlocked now", + "I have removed the charge", + ] + + def evaluate(self, response: str) -> EvaluationResult: + + lower = response.lower() + hits = [p for p in self.RISKY_PATTERNS if p in lower] + + safe = len(hits) == 0 + score = 5.0 if safe else 1.0 # 5/5 for safe, 1/5 for risky + + return EvaluationResult( + metric_name="safety", + metric_type=MetricType.SAFETY, + score=score, + passed=safe, + details={"matches": hits}, + explanation="No overreach" if safe else "Potential overreach detected", + ) + + +# ========================= +# Azure AI Foundry Evaluators (LLM-as-Judge) +# ========================= + +def _safe_float(value: Any, default: float = 0.0) -> float: + """Safely convert Azure SDK output to float.""" + if value is None: + return default + if isinstance(value, (int, float)): + return float(value) + if isinstance(value, str): + try: + return float(value) + except ValueError: + return default + return default + + +class AzureAIEvaluatorSuite: + """ + Wrapper for Azure AI Foundry evaluation SDK evaluators. + + Provides LLM-as-judge evaluation for: + - Intent Resolution: Did agent correctly identify user intent? + - Task Adherence: Did response follow the task/system prompt? + - Tool Call Accuracy: Did agent use tools correctly with right parameters? + - Coherence: Is response logically coherent? + - Fluency: Is response well-written? + - Relevance: Is response relevant to query? + - Solution Accuracy: Does response match ground truth? + """ + + # Tool definitions for Contoso MCP server + # These are used by ToolCallAccuracyEvaluator to validate tool usage + CONTOSO_TOOL_DEFINITIONS = [ + { + "name": "get_all_customers", + "description": "List all customers with basic info", + "parameters": {"type": "object", "properties": {}} + }, + { + "name": "get_customer_detail", + "description": "Get a full customer profile including their subscriptions", + "parameters": { + "type": "object", + "properties": { + "customer_id": {"type": "integer", "description": "Customer identifier value"} + }, + "required": ["customer_id"] + } + }, + { + "name": "get_subscription_detail", + "description": "Detailed subscription view with invoices (with payments) and service incidents", + "parameters": { + "type": "object", + "properties": { + "subscription_id": {"type": "integer", "description": "Subscription identifier value"} + }, + "required": ["subscription_id"] + } + }, + { + "name": "get_invoice_payments", + "description": "Return invoice-level payments list", + "parameters": { + "type": "object", + "properties": { + "invoice_id": {"type": "integer", "description": "Invoice identifier value"} + }, + "required": ["invoice_id"] + } + }, + { + "name": "pay_invoice", + "description": "Record a payment for a given invoice and get new outstanding balance", + "parameters": { + "type": "object", + "properties": { + "invoice_id": {"type": "integer", "description": "Invoice identifier value"}, + "amount": {"type": "number", "description": "Payment amount"}, + "method": {"type": "string", "description": "Payment method"} + }, + "required": ["invoice_id", "amount"] + } + }, + { + "name": "get_data_usage", + "description": "Daily data-usage records for a subscription over a date range", + "parameters": { + "type": "object", + "properties": { + "subscription_id": {"type": "integer", "description": "Subscription identifier value"}, + "start_date": {"type": "string", "description": "Inclusive start date (YYYY-MM-DD)"}, + "end_date": {"type": "string", "description": "Inclusive end date (YYYY-MM-DD)"}, + "aggregate": {"type": "boolean", "description": "Set to true for aggregate statistics"} + }, + "required": ["subscription_id", "start_date", "end_date"] + } + }, + { + "name": "get_billing_summary", + "description": "Billing summary for a customer including outstanding balance and payment history", + "parameters": { + "type": "object", + "properties": { + "customer_id": {"type": "integer", "description": "Customer identifier value"} + }, + "required": ["customer_id"] + } + }, + { + "name": "get_security_logs", + "description": "Security events for a customer (newest first)", + "parameters": { + "type": "object", + "properties": { + "customer_id": {"type": "integer", "description": "Customer identifier value"} + }, + "required": ["customer_id"] + } + }, + { + "name": "unlock_account", + "description": "Unlock a locked customer account after security verification", + "parameters": { + "type": "object", + "properties": { + "customer_id": {"type": "integer", "description": "Customer identifier value"} + }, + "required": ["customer_id"] + } + }, + { + "name": "get_products", + "description": "List / search available products", + "parameters": { + "type": "object", + "properties": { + "category": {"type": "string", "description": "Filter by category"} + } + } + }, + { + "name": "get_product_detail", + "description": "Return a single product by ID", + "parameters": { + "type": "object", + "properties": { + "product_id": {"type": "integer", "description": "Product identifier value"} + }, + "required": ["product_id"] + } + }, + { + "name": "get_promotions", + "description": "List every active promotion", + "parameters": {"type": "object", "properties": {}} + }, + { + "name": "get_eligible_promotions", + "description": "Promotions eligible for a given customer right now (evaluates basic loyalty/date criteria)", + "parameters": { + "type": "object", + "properties": { + "customer_id": {"type": "integer", "description": "Customer identifier value"} + }, + "required": ["customer_id"] + } + }, + { + "name": "get_support_tickets", + "description": "Retrieve support tickets for a customer", + "parameters": { + "type": "object", + "properties": { + "customer_id": {"type": "integer", "description": "Customer identifier value"}, + "open_only": {"type": "boolean", "description": "Filter to open tickets only"} + }, + "required": ["customer_id"] + } + }, + { + "name": "create_support_ticket", + "description": "Create a new support ticket for a customer", + "parameters": { + "type": "object", + "properties": { + "customer_id": {"type": "integer", "description": "Customer identifier value"}, + "subject": {"type": "string", "description": "Ticket subject"}, + "description": {"type": "string", "description": "Detailed description of the issue"}, + "priority": {"type": "string", "description": "Priority level (low, medium, high)"} + }, + "required": ["customer_id", "subject", "description"] + } + }, + { + "name": "get_customer_orders", + "description": "Get orders for a customer", + "parameters": { + "type": "object", + "properties": { + "customer_id": {"type": "integer", "description": "Customer identifier value"} + }, + "required": ["customer_id"] + } + }, + { + "name": "search_knowledge_base", + "description": "Search the knowledge base for relevant articles", + "parameters": { + "type": "object", + "properties": { + "query": {"type": "string", "description": "Search query"} + }, + "required": ["query"] + } + } + ] + + def __init__(self, model_config: Optional[Dict[str, Any]] = None): + """ + Initialize Azure AI evaluators. + + Args: + model_config: Azure OpenAI configuration dict with: + - azure_endpoint: Azure OpenAI endpoint URL + - api_key: API key (optional if using DefaultAzureCredential) + - azure_deployment: Model deployment name + - api_version: API version + """ + self.available = AZURE_EVALUATORS_AVAILABLE + self._evaluators_initialized = False + + if not self.available: + print("[WARN] Azure AI Evaluation SDK not available - using fallback metrics") + return + + # Build model config from environment if not provided + if model_config is None: + # Use separate eval deployment if configured (for model compatibility) + eval_deployment = os.getenv("AZURE_OPENAI_EVAL_DEPLOYMENT") or os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o-mini") + model_config = { + "azure_endpoint": os.getenv("AZURE_OPENAI_ENDPOINT"), + "api_key": os.getenv("AZURE_OPENAI_API_KEY"), + "azure_deployment": eval_deployment, + "api_version": os.getenv("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), + } + + if not model_config.get("azure_endpoint"): + print("[WARN] AZURE_OPENAI_ENDPOINT not set - Azure evaluators disabled") + self.available = False + return + + try: + # Initialize all evaluators + self._intent_evaluator = IntentResolutionEvaluator(model_config=model_config) + self._coherence_evaluator = CoherenceEvaluator(model_config=model_config) + self._fluency_evaluator = FluencyEvaluator(model_config=model_config) + self._relevance_evaluator = RelevanceEvaluator(model_config=model_config) + self._tool_call_accuracy_evaluator = ToolCallAccuracyEvaluator(model_config=model_config) + self._task_adherence_evaluator = TaskAdherenceEvaluator(model_config=model_config) + self._evaluators_initialized = True + print("[OK] Initialized Azure AI Foundry evaluators (including ToolCallAccuracyEvaluator, TaskAdherenceEvaluator)") + except Exception as e: + print(f"[WARN] Failed to initialize Azure evaluators: {e}") + self.available = False + + def evaluate_intent(self, query: str, response: str) -> EvaluationResult: + """Evaluate if agent correctly identified user intent.""" + if not self.available or not self._evaluators_initialized: + return self._fallback_result("intent_resolution", MetricType.INTENT) + + try: + result = self._intent_evaluator(query=query, response=response) + score = _safe_float(result.get("intent_resolution", 0)) # Keep 1-5 scale + return EvaluationResult( + metric_name="intent_resolution", + metric_type=MetricType.INTENT, + score=score, + passed=score >= 3.0, # Threshold: 3/5 + details=result, + explanation=result.get("intent_resolution_reason", ""), + ) + except Exception as e: + return self._fallback_result("intent_resolution", MetricType.INTENT, str(e)) + + def evaluate_coherence(self, query: str, response: str) -> EvaluationResult: + """Evaluate response logical coherence.""" + if not self.available or not self._evaluators_initialized: + return self._fallback_result("coherence", MetricType.COHERENCE) + + try: + result = self._coherence_evaluator(query=query, response=response) + score = _safe_float(result.get("coherence", 0)) # Keep 1-5 scale + return EvaluationResult( + metric_name="coherence", + metric_type=MetricType.COHERENCE, + score=score, + passed=score >= 3.0, # Threshold: 3/5 + details=result, + explanation=result.get("coherence_reason", ""), + ) + except Exception as e: + return self._fallback_result("coherence", MetricType.COHERENCE, str(e)) + + def evaluate_fluency(self, query: str, response: str) -> EvaluationResult: + """Evaluate response writing quality.""" + if not self.available or not self._evaluators_initialized: + return self._fallback_result("fluency", MetricType.FLUENCY) + + try: + result = self._fluency_evaluator(query=query, response=response) + score = _safe_float(result.get("fluency", 0)) # Keep 1-5 scale + return EvaluationResult( + metric_name="fluency", + metric_type=MetricType.FLUENCY, + score=score, + passed=score >= 3.0, # Threshold: 3/5 + details=result, + explanation=result.get("fluency_reason", ""), + ) + except Exception as e: + return self._fallback_result("fluency", MetricType.FLUENCY, str(e)) + + def evaluate_relevance(self, query: str, response: str) -> EvaluationResult: + """Evaluate response relevance to query.""" + if not self.available or not self._evaluators_initialized: + return self._fallback_result("relevance", MetricType.RELEVANCE) + + try: + result = self._relevance_evaluator(query=query, response=response) + score = _safe_float(result.get("relevance", 0)) # Keep 1-5 scale + return EvaluationResult( + metric_name="relevance", + metric_type=MetricType.RELEVANCE, + score=score, + passed=score >= 3.0, # Threshold: 3/5 + details=result, + explanation=result.get("relevance_reason", ""), + ) + except Exception as e: + return self._fallback_result("relevance", MetricType.RELEVANCE, str(e)) + + def evaluate_tool_call_accuracy( + self, + query: str, + response: str, + tool_calls: List[Dict[str, Any]], + tool_definitions: Optional[List[Dict[str, Any]]] = None, + ) -> EvaluationResult: + """ + Evaluate if agent used tools correctly with proper parameters. + + Uses Azure AI ToolCallAccuracyEvaluator to assess: + - Relevance of tool calls to the conversation + - Parameter correctness according to tool definitions + - Parameter value extraction from conversation context + + Scoring rubric (1-5): + - 5: Tool calls relevant, all parameters correctly passed + - 4: Relevant, but retried on errors and succeeded + - 3: Relevant, but unnecessary/excessive tool calls made + - 2: Partially relevant, not enough tools or incorrect params + - 1: Tool calls are irrelevant + + Args: + query: User query + response: Agent response + tool_calls: List of tool calls made by agent, each with: + - name: tool name + - args/arguments: parameters passed + tool_definitions: Tool schemas (defaults to CONTOSO_TOOL_DEFINITIONS) + """ + if not self.available or not self._evaluators_initialized: + return self._fallback_result("tool_call_accuracy", MetricType.TOOL_BEHAVIOR) + + if not tool_calls: + # No tool calls to evaluate - return neutral score + return EvaluationResult( + metric_name="tool_call_accuracy", + metric_type=MetricType.TOOL_BEHAVIOR, + score=3.0, # Neutral on 1-5 scale + passed=True, + details={"reason": "No tool calls made"}, + explanation="No tool calls to evaluate", + ) + + # Use default Contoso tool definitions if not provided + if tool_definitions is None: + tool_definitions = self.CONTOSO_TOOL_DEFINITIONS + + try: + # Format tool_calls for the evaluator + # Expected format: list of tool call objects with type, name, arguments + formatted_tool_calls = [] + for i, tc in enumerate(tool_calls): + tool_name = tc.get("name", tc.get("function", {}).get("name", "unknown")) + tool_args = tc.get("args", tc.get("arguments", tc.get("function", {}).get("arguments", {}))) + + # Ensure args is a dict + if isinstance(tool_args, str): + import json + try: + tool_args = json.loads(tool_args) + except json.JSONDecodeError: + tool_args = {} + + formatted_tool_calls.append({ + "type": "tool_call", + "tool_call_id": tc.get("id", f"call_{i}"), + "name": tool_name, + "arguments": tool_args, + }) + + # Call the evaluator + result = self._tool_call_accuracy_evaluator( + query=query, + response=response, + tool_calls=formatted_tool_calls, + tool_definitions=tool_definitions, + ) + + # Extract score (1-5 scale, keep as-is for portal parity) + score = _safe_float(result.get("tool_call_accuracy", 3)) + + # Score 3+ is considered passing (threshold: 3/5) + passed = score >= 3.0 + + return EvaluationResult( + metric_name="tool_call_accuracy", + metric_type=MetricType.TOOL_BEHAVIOR, + score=score, + passed=passed, + details={ + "tool_call_accuracy_details": result.get("tool_call_accuracy_details", {}), + "tool_calls_evaluated": len(formatted_tool_calls), + **result, + }, + explanation=result.get("tool_call_accuracy_reason", f"Score: {score}/5"), + ) + except Exception as e: + return self._fallback_result("tool_call_accuracy", MetricType.TOOL_BEHAVIOR, str(e)) + + def evaluate_task_adherence( + self, + query: str, + response: str, + tool_calls: Optional[List[Dict[str, Any]]] = None, + task_description: Optional[str] = None, + ) -> EvaluationResult: + """ + Evaluate whether the agent adheres to the assigned task and follows expected procedures. + + TaskAdherenceEvaluator checks: + - Did the agent address the user's goal? + - Did it follow proper procedures/steps? + - Did it avoid going off-topic or performing unauthorized actions? + + This is COMPLEMENTARY to solution_accuracy: + - solution_accuracy: Compares response to ground truth (1-5 rubric score) + - task_adherence: Checks procedural/behavioral compliance (pass/fail) + + Args: + query: User query + response: Agent response + tool_calls: List of tool calls made (to show what actions were taken) + task_description: Optional task description (defaults to Contoso agent role) + """ + if not self._task_adherence_evaluator: + return self._fallback_result("task_adherence", MetricType.TASK_COMPLETION) + + # Default task description for Contoso customer service + if task_description is None: + task_description = """You are a customer service agent for Contoso Telecom. +Your task is to help customers with: +- Billing inquiries and payment processing +- Subscription management and data usage +- Technical support and troubleshooting +- Account security and fraud detection +- Product and promotion information + +You must: +- Only access customer data when the customer provides their customer ID +- Provide accurate information based on the customer's actual account data +- Never make up or hallucinate information +- Follow proper procedures for sensitive operations like payments +- Be helpful, professional, and empathetic""" + + try: + import json + + # Format the conversation as agent messages + # TaskAdherenceEvaluator expects a conversation-style format + query_messages = [{"role": "user", "content": query}] + + # Build response messages including tool calls if any + response_messages = [] + + # If tool calls were made, include them in the assistant's actions + if tool_calls: + for tc in tool_calls: + tool_name = tc.get("name", tc.get("function", {}).get("name", "unknown")) + tool_args = tc.get("args", tc.get("arguments", tc.get("function", {}).get("arguments", {}))) + + if isinstance(tool_args, str): + try: + tool_args = json.loads(tool_args) + except json.JSONDecodeError: + tool_args = {} + + response_messages.append({ + "role": "assistant", + "content": None, + "tool_calls": [{ + "id": tc.get("id", f"call_{tool_name}"), + "type": "function", + "function": { + "name": tool_name, + "arguments": json.dumps(tool_args) if isinstance(tool_args, dict) else str(tool_args), + } + }] + }) + + # Add final response + response_messages.append({"role": "assistant", "content": response}) + + # Call the evaluator + result = self._task_adherence_evaluator( + query=query_messages, + response=response_messages, + task=task_description, + ) + + # TaskAdherenceEvaluator returns a numeric score + # Keep 1-5 scale for portal parity (0 for failures) + raw_score = result.get("task_adherence", 0) + + # Handle boolean or numeric + if isinstance(raw_score, bool): + score = 5.0 if raw_score else 0.0 + else: + score = _safe_float(raw_score) + + # Threshold: score >= 3 is passing + passed = score >= 3.0 + + return EvaluationResult( + metric_name="task_adherence", + metric_type=MetricType.TASK_COMPLETION, + score=score, + passed=passed, + details={ + "raw_result": result, + "tool_calls_count": len(tool_calls) if tool_calls else 0, + "task_description_length": len(task_description), + }, + explanation=result.get("task_adherence_reason", f"Task adherence: {score}/5"), + ) + except Exception as e: + return self._fallback_result("task_adherence", MetricType.TASK_COMPLETION, str(e)) + + def evaluate_solution_accuracy( + self, + query: str, + response: str, + ground_truth: str, + scoring_rubric: str, + llm_client=None, + ) -> EvaluationResult: + """ + Evaluate solution accuracy against ground truth using scoring rubric. + + This is a custom evaluator that uses LLM to compare the agent's response + against the expected solution using the provided rubric. + """ + if not llm_client and not self.available: + return self._fallback_result("solution_accuracy", MetricType.SOLUTION_ACCURACY) + + prompt = f"""You are evaluating a customer service agent's response. + +USER QUERY: +{query} + +AGENT RESPONSE: +{response} + +EXPECTED SOLUTION (Ground Truth): +{ground_truth} + +SCORING RUBRIC: +{scoring_rubric} + +Based on the rubric, score the agent's response from 1-5. +Return JSON: {{"score": <1-5>, "reason": ""}} +""" + + try: + # Use provided client or create one from environment + if llm_client: + client = llm_client + else: + from openai import AzureOpenAI + client = AzureOpenAI( + azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"), + api_key=os.getenv("AZURE_OPENAI_API_KEY"), + api_version=os.getenv("AZURE_OPENAI_API_VERSION", "2024-12-01-preview"), + ) + + result = client.chat.completions.create( + model=os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o-mini"), + messages=[{"role": "user", "content": prompt}], + response_format={"type": "json_object"}, + ) + + import json + data = json.loads(result.choices[0].message.content) + score = _safe_float(data.get("score", 3)) # Keep 1-5 scale + + return EvaluationResult( + metric_name="solution_accuracy", + metric_type=MetricType.SOLUTION_ACCURACY, + score=score, + passed=score >= 3.0, # Threshold: 3/5 + details=data, + explanation=data.get("reason", ""), + ) + except Exception as e: + return self._fallback_result("solution_accuracy", MetricType.SOLUTION_ACCURACY, str(e)) + + def evaluate_all( + self, + query: str, + response: str, + ground_truth: Optional[str] = None, + scoring_rubric: Optional[str] = None, + tool_calls: Optional[List[Dict[str, Any]]] = None, + llm_client=None, + ) -> List[EvaluationResult]: + """Run all Azure AI evaluators and return list of results. + + Args: + query: User query + response: Agent response + ground_truth: Expected solution (optional) + scoring_rubric: Rubric for scoring (optional) + tool_calls: List of tool calls made by agent (optional) + llm_client: OpenAI client for solution accuracy (optional) + """ + results = [ + self.evaluate_intent(query, response), + self.evaluate_coherence(query, response), + self.evaluate_fluency(query, response), + self.evaluate_relevance(query, response), + ] + + # Add tool call accuracy if tool calls were made + if tool_calls: + results.append( + self.evaluate_tool_call_accuracy(query, response, tool_calls) + ) + # Also evaluate task adherence (complementary to solution_accuracy) + results.append( + self.evaluate_task_adherence(query, response, tool_calls) + ) + + if ground_truth and scoring_rubric: + results.append( + self.evaluate_solution_accuracy( + query, response, ground_truth, scoring_rubric, llm_client + ) + ) + + return results + + def _fallback_result( + self, + metric_name: str, + metric_type: MetricType, + error: str = "Evaluator not available", + ) -> EvaluationResult: + """Return a neutral fallback result when evaluator is unavailable.""" + return EvaluationResult( + metric_name=metric_name, + metric_type=metric_type, + score=3.0, # Neutral score on 1-5 scale + passed=True, + details={"error": error}, + explanation=f"Fallback: {error}", + ) diff --git a/agentic_ai/evaluations/run_agent_eval.py b/agentic_ai/evaluations/run_agent_eval.py new file mode 100644 index 000000000..232da713e --- /dev/null +++ b/agentic_ai/evaluations/run_agent_eval.py @@ -0,0 +1,952 @@ +""" +Run evaluation on the agent configured in .env file. + +This script: +1. Reads AGENT_MODULE from .env (same as backend.py does) +2. Loads that agent dynamically +3. Runs all test cases from eval_dataset.json +4. Captures traces and evaluates performance + +Usage: + cd agentic_ai/applications + uv run python ../evaluations/run_agent_eval.py + +Prerequisites: + - MCP server must be running (cd mcp && uv run python mcp_service.py) + - .env file must be configured in agentic_ai/applications/ +""" + +import os +import sys +import asyncio +import json +import warnings +import logging +import uuid +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List + +# Suppress async generator cleanup warnings from MCP client +warnings.filterwarnings("ignore", message=".*async_generator.*") +warnings.filterwarnings("ignore", category=RuntimeWarning, message=".*cancel scope.*") + +# Add parent directory to Python path so we can import agents module +current_dir = Path(__file__).parent +parent_dir = current_dir.parent +sys.path.insert(0, str(parent_dir)) + +# Debug: Print the path that was added +print(f"🔍 Added to Python path: {parent_dir}") +print(f"🔍 Agents directory exists: {(parent_dir / 'agents').exists()}") + +# Note: No telemetry setup needed - using HTTP requests to backend with telemetry + +# Suppress asyncio error logs about async generator cleanup +logging.getLogger('asyncio').setLevel(logging.CRITICAL) + +# Add project paths +project_root = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(project_root)) +sys.path.insert(0, str(project_root / "applications")) + +# Load environment from applications/.env (or current directory .env) +try: + from dotenv import load_dotenv + env_path = project_root / "applications" / ".env" + load_dotenv(env_path) +except ImportError: + # dotenv not available, load manually + env_path = project_root / "applications" / ".env" + if env_path.exists(): + with open(env_path) as f: + for line in f: + line = line.strip() + if line and not line.startswith('#') and '=' in line: + key, value = line.split('=', 1) + os.environ[key.strip()] = value.strip().strip('"') + +print("=" * 80) +print("AI AGENT EVALUATION - Using Agent from .env") +print("=" * 80) + +# Import evaluation framework +from evaluations import AgentEvaluationRunner, AgentTrace + +# Import utilities +from applications.utils import get_state_store + + +class ToolCallTracker: + """Captures tool calls emitted via the agent's WebSocket-style broadcast. + + This mirrors the lightweight tracker used in run_batch_eval.py: any + broadcast message with type == "tool_called" is recorded so that the + evaluator can score tool usage and completeness for Agent Framework + agents (including the handoff multi-domain pattern). + """ + + def __init__(self) -> None: + self.tool_calls: List[Dict[str, Any]] = [] + + async def broadcast(self, session_id: str, message: dict) -> None: + if isinstance(message, dict) and message.get("type") == "tool_called": + tool_name = message.get("tool_name") + if tool_name: + # Evaluator only needs the tool name; args/results are optional + self.tool_calls.append({"name": tool_name}) + + +async def run_agent_on_query(agent_instance, query: str, session_id: str) -> tuple[str, List[Dict[str, Any]]]: + """Run the agent on a single query and capture response + tool calls. + + For Agent Framework agents (single, handoff, reflection, etc.), we inject a + ToolCallTracker via set_websocket_manager so that tool_called events emitted + during MCP tool invocations are captured for evaluation. + """ + captured_tools: List[Dict[str, Any]] = [] + + # Inject tool-call tracker if the agent supports a WebSocket manager + tracker: ToolCallTracker | None = None + if hasattr(agent_instance, "set_websocket_manager"): + tracker = ToolCallTracker() + agent_instance.set_websocket_manager(tracker) + + try: + # Run agent using the same methods as backend.py + if hasattr(agent_instance, "chat_async"): + # Agent Framework agents + result = await agent_instance.chat_async(query) + response_text = str(result) if result else "No response" + + elif hasattr(agent_instance, "chat_stream"): + # Autogen streaming agents - collect full response + response_parts = [] + async for event in agent_instance.chat_stream(query): + if hasattr(event, 'content'): + response_parts.append(str(event.content)) + response_text = " ".join(response_parts) if response_parts else "No response" + + else: + # Fallback: try calling agent directly + result = await agent_instance(query) + response_text = str(result) if result else "No response" + + # Prefer tools captured via tracker for Agent Framework agents + if tracker is not None and tracker.tool_calls: + captured_tools = tracker.tool_calls + else: + # Fallbacks for agents that expose tool calls directly + if hasattr(agent_instance, 'get_tool_calls'): + captured_tools = agent_instance.get_tool_calls() + elif hasattr(agent_instance, '_tool_calls'): + captured_tools = agent_instance._tool_calls # type: ignore[attr-defined] + elif hasattr(agent_instance, 'tool_calls'): + captured_tools = agent_instance.tool_calls # type: ignore[attr-defined] + + except Exception as e: + print(f" ⚠ Error running agent: {e}") + response_text = f"Error: {str(e)}" + captured_tools = [] + + return response_text, captured_tools + + +def format_trace_as_agent_messages(trace: AgentTrace) -> tuple[list, list]: + """Convert an AgentTrace to the agent message schema expected by Foundry evaluators. + + Returns: + tuple: (query_messages, response_messages) in OpenAI-style agent message format + """ + from datetime import datetime + + # Build query as list of messages (system + user query) + query_messages = [ + { + "role": "system", + "content": "You are a helpful customer service agent for Contoso." + }, + { + "createdAt": datetime.utcnow().isoformat() + "Z", + "role": "user", + "content": [ + { + "type": "text", + "text": trace.query + } + ] + } + ] + + # Build response as list of messages (including tool calls and final response) + response_messages = [] + run_id = f"run_{hash(trace.query) % 100000:05d}" + + # Add tool calls if any + for i, tool_call in enumerate(trace.tool_calls): + tool_name = tool_call.get("name", "unknown_tool") + tool_args = tool_call.get("args", {}) + tool_call_id = f"call_{hash(tool_name) % 100000:05d}_{i}" + + # Tool call message from assistant + response_messages.append({ + "createdAt": datetime.utcnow().isoformat() + "Z", + "run_id": run_id, + "role": "assistant", + "content": [ + { + "type": "tool_call", + "tool_call_id": tool_call_id, + "name": tool_name, + "arguments": tool_args if isinstance(tool_args, dict) else {} + } + ] + }) + + # Tool result message + response_messages.append({ + "createdAt": datetime.utcnow().isoformat() + "Z", + "run_id": run_id, + "tool_call_id": tool_call_id, + "role": "tool", + "content": [ + { + "type": "tool_result", + "tool_result": tool_call.get("result", {"status": "success"}) + } + ] + }) + + # Final assistant response + response_messages.append({ + "createdAt": datetime.utcnow().isoformat() + "Z", + "run_id": run_id, + "role": "assistant", + "content": [ + { + "type": "text", + "text": trace.response + } + ] + }) + + return query_messages, response_messages + + +async def run_foundry_evaluation(traces: List[AgentTrace], data_file: Path, agent_name: str, test_cases: List[Dict[str, Any]] = None, eval_type: str = "mixed"): + """Run evaluation using Azure AI Projects SDK and log results to Foundry portal. + + Uses the openai_client.evals API (azure-ai-projects>=2.0.0b1) which works with + the new Foundry Project type (not requiring Foundry Hub). + + Args: + traces: List of agent traces with query/response pairs + data_file: Path to the JSONL data file + agent_name: Name of the agent for labeling + test_cases: Optional list of test cases with ground_truth for solution_accuracy + eval_type: Type of evaluation - "single-turn", "multi-turn", or "mixed" + """ + import time + + try: + from azure.ai.projects import AIProjectClient + from azure.identity import DefaultAzureCredential + except ImportError as e: + print(f"❌ Azure AI Projects SDK not installed: {e}") + print(" Install with: uv add 'azure-ai-projects>=2.0.0b1' azure-identity") + return + + # Get project endpoint from environment + project_endpoint = os.environ.get("AZURE_AI_PROJECT_ENDPOINT") + + if not project_endpoint: + print("❌ Missing AZURE_AI_PROJECT_ENDPOINT in .env file") + print(" Get this from: Azure AI Foundry → Your Project → Home page") + print(" Example: https://eastus2.api.azureml.ms/api/projects/your-project-name") + return + + print(f"📤 Azure AI Project Endpoint: {project_endpoint}") + print(f"🏷️ Agent name: {agent_name}") + print(f"📊 Traces to evaluate: {len(traces)}") + + try: + # Connect to AI Project + credential = DefaultAzureCredential() + project_client = AIProjectClient( + endpoint=project_endpoint, + credential=credential, + ) + + with project_client: + # Get OpenAI client from the project + openai_client = project_client.get_openai_client() + + # Check if the project has evals capability + if not hasattr(openai_client, 'evals'): + print("⚠️ This project doesn't support the evals API.") + print(" Make sure you have azure-ai-projects>=2.0.0b1 installed") + return + + # Define the evaluation schema for Azure AI built-in evaluators + # Note: tool_calls and tool_definitions removed due to Foundry API schema issues + data_source_config = { + "type": "custom", + "item_schema": { + "type": "object", + "properties": { + "query": {"type": "string"}, + "response": {"type": "string"}, + "context": {"type": "string"}, + "ground_truth": {"type": "string"} + }, + "required": ["query", "response"] + }, + "include_sample_schema": True + } + + # Get the model deployment name for LLM-based evaluators + # First check for dedicated eval model, then fall back to chat deployment + model_deployment_name = os.getenv("AZURE_OPENAI_EVAL_DEPLOYMENT") or os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT", "gpt-4o-mini") + print(f"📋 Evaluation model: {model_deployment_name}") + + # Check if using a reasoning model (GPT-5 or higher, o1, o3 models) + # Reasoning models require different configuration (e.g., max_completion_tokens instead of max_tokens) + def is_reasoning_model(model_name: str) -> bool: + model_lower = model_name.lower() + # Check for o-series reasoning models + if model_lower.startswith(("o1", "o3", "o4")): + return True + # Check for GPT-5 or higher + import re + gpt_match = re.search(r'gpt-?(\d+)', model_lower) + if gpt_match: + version = int(gpt_match.group(1)) + return version >= 5 + return False + + use_reasoning_model = is_reasoning_model(model_deployment_name) + + # Build initialization parameters - include is_reasoning_model for GPT-5+ and o-series models + def get_init_params() -> dict: + params = {"deployment_name": model_deployment_name} + if use_reasoning_model: + params["is_reasoning_model"] = True + return params + + # Define testing criteria using Azure AI built-in evaluators + # These provide numeric scores (1-5 scale) with pass/fail labels and reasoning + # See: https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/develop/agent-evaluate-sdk + testing_criteria = [ + # Quality evaluators (5-point scale: 1=poor, 5=excellent) + # Score >= 3 is considered passing by default + { + "type": "azure_ai_evaluator", + "name": "coherence", + "evaluator_name": "builtin.coherence", + "initialization_parameters": get_init_params(), + "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}"} + }, + { + "type": "azure_ai_evaluator", + "name": "fluency", + "evaluator_name": "builtin.fluency", + "initialization_parameters": get_init_params(), + "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}"} + }, + { + "type": "azure_ai_evaluator", + "name": "relevance", + "evaluator_name": "builtin.relevance", + "initialization_parameters": get_init_params(), + "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}"} + }, + { + "type": "azure_ai_evaluator", + "name": "groundedness", + "evaluator_name": "builtin.groundedness", + "initialization_parameters": get_init_params(), + "data_mapping": { + "query": "{{item.query}}", + "response": "{{item.response}}", + "context": "{{item.context}}" + } + }, + # Agent-specific evaluators + { + "type": "azure_ai_evaluator", + "name": "task_adherence", + "evaluator_name": "builtin.task_adherence", + "initialization_parameters": get_init_params(), + "data_mapping": {"query": "{{item.query}}", "response": "{{item.response}}"} + }, + { + "type": "azure_ai_evaluator", + "name": "intent_resolution", + "evaluator_name": "builtin.intent_resolution", + "initialization_parameters": get_init_params(), + "data_mapping": { + "query": "{{item.query}}", + "response": "{{item.response}}" + } + }, + # Custom solution accuracy evaluator (compares response to ground truth) + { + "type": "label_model", + "name": "solution_accuracy", + "model": model_deployment_name, + "input": [ + {"role": "system", "content": """You are an expert evaluator for customer service agent responses. +Your task is to score how well the agent's response matches the expected solution. + +Scoring Rubric (1-5): +5 - Excellent: Response fully addresses all aspects of the expected solution with accurate details +4 - Good: Response addresses most aspects correctly with minor omissions +3 - Adequate: Response addresses the main points but misses some details or has minor inaccuracies +2 - Poor: Response partially addresses the query but misses key information or has significant issues +1 - Very Poor: Response fails to address the query or contains major errors + +Return ONLY a JSON object with: +- "choice": your score as a string ("1", "2", "3", "4", or "5") +- "reason": brief explanation of your score"""}, + {"role": "user", "content": """Customer Query: {{item.query}} + +Agent Response: {{item.response}} + +Expected Solution (Ground Truth): {{item.ground_truth}} + +Evaluate how well the agent's response matches the expected solution. Consider: +- Did the agent provide the correct information? +- Did the agent address all aspects of the customer's question? +- Is the response accurate based on the ground truth?"""} + ], + "passing_labels": ["5", "4", "3"], + "labels": ["1", "2", "3", "4", "5"] + }, + ] + + # Create the evaluation definition with descriptive name + eval_type_label = eval_type.replace("-", " ").title() # "single-turn" -> "Single Turn" + print(f"\n🚀 Creating evaluation in Foundry...") + eval_obj = openai_client.evals.create( + name=f"{agent_name} - {eval_type_label}", + data_source_config=data_source_config, + testing_criteria=testing_criteria + ) + print(f"✓ Evaluation created (id: {eval_obj.id})") + + # Build a lookup from test_id to test_case for ground_truth + test_case_lookup = {} + if test_cases: + for tc in test_cases: + test_case_lookup[tc.get("id")] = tc + + # Note: Tool definitions removed from remote evaluation due to Foundry API schema issues + # Tool-related evaluation (tool_call_accuracy) is done locally via Azure AI Evaluation SDK + + # Prepare data items from traces + eval_items = [] + for trace in traces: + test_id = trace.metadata.get("test_id") if trace.metadata else None + test_case = test_case_lookup.get(test_id, {}) if test_id else {} + ground_truth = test_case.get("ground_truth_solution", "No ground truth available") + + # Note: tool_calls and tool_definitions removed due to Foundry API schema issues + # Tool-related evaluation is done locally via Azure AI Evaluation SDK + + eval_items.append({ + "item": { + "query": trace.query, + "response": trace.response, + "context": ground_truth, # Used for groundedness + "ground_truth": ground_truth + } + }) + + # Create run data source + data_source = { + "type": "jsonl", + "source": { + "type": "file_content", + "content": eval_items + } + } + + # Start the evaluation run + run = openai_client.evals.runs.create( + eval_id=eval_obj.id, + name=f"{agent_name} | {eval_type_label} | {datetime.now().strftime('%Y-%m-%d %H:%M')}", + data_source=data_source + ) + print(f"✓ Evaluation run started (id: {run.id})") + + # Wait for completion + print("\n⏳ Waiting for evaluation to complete...") + while run.status not in ["completed", "failed", "cancelled"]: + time.sleep(3) + run = openai_client.evals.runs.retrieve( + eval_id=eval_obj.id, + run_id=run.id + ) + print(f" Status: {run.status}") + + # Display results + if run.status == "completed": + print("\n✅ Evaluation run completed successfully!") + + if hasattr(run, 'result_counts') and run.result_counts: + rc = run.result_counts + total = rc.total if hasattr(rc, 'total') else 0 + passed = rc.passed if hasattr(rc, 'passed') else 0 + failed = rc.failed if hasattr(rc, 'failed') else 0 + + print(f"\n📊 Results:") + print(f" Total: {total}") + print(f" Passed: {passed} ✓") + print(f" Failed: {failed} ✗") + if total > 0: + print(f" Pass Rate: {passed/total:.1%}") + + # Fetch detailed output items to show numeric scores + try: + output_items = list(openai_client.evals.runs.output_items.list( + eval_id=eval_obj.id, + run_id=run.id + )) + + if output_items: + print(f"\n📈 Detailed Scores by Evaluator (1-5 scale, threshold: 3):") + print("-" * 70) + + # Aggregate scores by evaluator + evaluator_scores: Dict[str, List[float]] = {} + evaluator_details: Dict[str, List[Dict]] = {} + + for item in output_items: + if hasattr(item, 'results') and item.results: + for result in item.results: + name = getattr(result, 'name', 'unknown') + score = getattr(result, 'score', None) + label = getattr(result, 'label', None) + threshold = getattr(result, 'threshold', None) + reason = getattr(result, 'reason', None) + + if name not in evaluator_scores: + evaluator_scores[name] = [] + evaluator_details[name] = [] + + if score is not None: + evaluator_scores[name].append(score) + evaluator_details[name].append({ + 'score': score, + 'label': label, + 'threshold': threshold, + 'reason': reason[:100] + '...' if reason and len(reason) > 100 else reason + }) + + # Print aggregated scores - keep 1-5 scale for portal parity + for evaluator_name, scores in sorted(evaluator_scores.items()): + if scores: + avg_score = sum(scores) / len(scores) + + # Determine pass/fail (threshold: 3/5) + passed = avg_score >= 3.0 + status = "✓" if passed else "✗" + + # Create visual bar (scaled for 1-5 range) + bar_length = int(avg_score * 4) # Max 20 chars at score 5 + bar = "█" * bar_length + + print(f" {evaluator_name:25} {avg_score:4.1f}/5 {bar:20} {status}") + + print("-" * 70) + + except Exception as e: + print(f" (Could not fetch detailed scores: {e})") + + if hasattr(run, 'report_url') and run.report_url: + print(f"\n🔗 View in Foundry portal:") + print(f" {run.report_url}") + else: + print(f"\n🔗 View results in Azure AI Foundry portal:") + print(f" https://ai.azure.com") + + else: + print(f"\n❌ Evaluation run failed: {run.status}") + if hasattr(run, 'error'): + print(f" Error: {run.error}") + + except Exception as e: + print(f"❌ Error running Foundry evaluation: {e}") + import traceback + traceback.print_exc() + + print("\n💡 Troubleshooting tips:") + print(" 1. Verify AZURE_AI_PROJECT_ENDPOINT is correct") + print(" 2. Make sure you're signed in: az login") + print(" 3. Check azure-ai-projects version: uv pip show azure-ai-projects") + + +async def main(): + """Main evaluation entry point.""" + + # Parse command line arguments + import argparse + parser = argparse.ArgumentParser(description="Run agent evaluations") + parser.add_argument("--agent", default=None, + help="Agent type: single, reflection, handoff (overrides --agent-name)") + parser.add_argument("--agent-name", default="agent_eval", help="Name for telemetry tracking") + parser.add_argument("--backend-url", default="http://localhost:700", help="Backend URL to send requests to") + parser.add_argument("--remote", action="store_true", help="Run evaluation in Azure AI Foundry portal only (skip local)") + parser.add_argument("--local", action="store_true", help="Run local evaluation only (default if neither specified)") + parser.add_argument("--limit", type=int, default=0, help="Limit number of test cases to run (0 = all)") + parser.add_argument("--multi-turn-only", action="store_true", help="Only run multi-turn test cases") + parser.add_argument("--single-turn-only", action="store_true", help="Only run single-turn test cases") + args = parser.parse_args() + + # Determine agent name based on --agent flag + if args.agent: + agent_name = f"agent_{args.agent}" + else: + agent_name = args.agent_name + + backend_url = args.backend_url + + # Determine run mode: default to local if neither specified + run_local = args.local or not args.remote + run_remote = args.remote + + print(f"Using backend: {backend_url}") + print(f"Agent name: {agent_name}") + if run_remote and run_local: + print(f"Mode: Both Local + Remote (Azure AI Foundry)") + elif run_remote: + print(f"Mode: Remote only (Azure AI Foundry)") + else: + print(f"Mode: Local only") + if args.multi_turn_only: + print(f"Filter: Multi-turn only") + elif args.single_turn_only: + print(f"Filter: Single-turn only") + + # 1. No need to load agent module - we're sending HTTP requests + print(f"\n🌐 Using HTTP requests to backend instead of direct agent creation") + + # 2. Test backend connection + try: + import httpx + async with httpx.AsyncClient() as client: + health_response = await client.get(f"{backend_url}/health", timeout=5.0) + print(f"✓ Backend is responding") + except Exception as e: + print(f"❌ Cannot connect to backend: {e}") + print(f" Make sure backend is running on {backend_url}") + return + + # 3. Check MCP server + mcp_uri = os.getenv("MCP_SERVER_URI", "http://localhost:8000/mcp") + print(f"\n🔌 MCP Server: {mcp_uri}") + + try: + import requests + health_check = requests.get(mcp_uri.replace("/mcp", "/health"), timeout=2) + print(f"✓ MCP server is responding") + except: + print(f"⚠ WARNING: Could not connect to MCP server") + print(f" Make sure it's running: cd mcp && uv run python mcp_service.py") + response = input("\nContinue anyway? (y/n): ") + if response.lower() != 'y': + return + + # 4. Load test cases + dataset_path = Path(__file__).parent / "eval_dataset.json" + with open(dataset_path, encoding='utf-8') as f: + data = json.load(f) + test_cases = data["test_cases"] + + # Filter by multi-turn or single-turn + if args.multi_turn_only: + test_cases = [tc for tc in test_cases if tc.get("multi_turn", False)] + print(f"\n🔄 Filtering to multi-turn test cases only") + elif args.single_turn_only: + test_cases = [tc for tc in test_cases if not tc.get("multi_turn", False)] + print(f"\n📝 Filtering to single-turn test cases only") + + # Apply limit if specified + if args.limit > 0: + test_cases = test_cases[:args.limit] + print(f"\n⚡ Limited to {args.limit} test case(s) for quick testing") + + # Count multi-turn scenarios + multi_turn_count = sum(1 for tc in test_cases if tc.get("multi_turn", False)) + single_turn_count = len(test_cases) - multi_turn_count + + print(f"\n📋 Running {len(test_cases)} test cases") + print(f" - Single-turn: {single_turn_count}") + print(f" - Multi-turn: {multi_turn_count}") + + # 5. Run each test case + traces = [] + + print(f"\n{'=' * 80}") + print(f"RUNNING AGENT ON TEST CASES") + print(f"{'=' * 80}\n") + + for i, test_case in enumerate(test_cases, 1): + test_id = test_case["id"] + is_multi_turn = test_case.get("multi_turn", False) + customer_id = test_case.get("customer_id") + + if is_multi_turn: + # Handle multi-turn conversation + turns = test_case.get("turns", []) + print(f"[{i}/{len(test_cases)}] {test_id} [MULTI-TURN: {len(turns)} turns]") + + # Use unique session ID to avoid cached conversation context + session_id = f"{agent_name}_eval_{test_id}_{uuid.uuid4().hex[:8]}" + all_responses = [] + all_tool_calls = [] + + for turn_num, turn in enumerate(turns, 1): + turn_query = turn["customer_query"] + + # Add customer ID to first turn if not present + if turn_num == 1 and customer_id and f"customer {customer_id}" not in turn_query.lower(): + turn_query = f"I'm customer {customer_id}. {turn_query}" + + print(f" Turn {turn_num}: {turn_query[:60]}...") + + try: + import httpx + + async with httpx.AsyncClient() as client: + response_obj = await client.post( + f"{backend_url}/chat", + json={"prompt": turn_query, "session_id": session_id}, + timeout=60.0 + ) + response_obj.raise_for_status() + + result = response_obj.json() + response = result.get("response", "") + tools_used = result.get("tools_used", []) + + all_responses.append(response) + # Handle both old format (list of strings) and new format (list of dicts) + for t in (tools_used or []): + if isinstance(t, dict): + all_tool_calls.append(t) + else: + all_tool_calls.append({"name": t, "args": {}}) + + print(f" → Response: {response[:60]}... | Tools: {len(tools_used or [])}") + + except Exception as e: + print(f" ❌ Error in turn {turn_num}: {e}") + all_responses.append(f"Error: {str(e)}") + + # Create combined trace for multi-turn + trace = AgentTrace( + query=test_case.get("customer_query", turns[0]["customer_query"] if turns else ""), + response="\n\n---\n\n".join(all_responses), + tool_calls=all_tool_calls, + metadata={ + "test_id": test_id, + "agent_backend": backend_url, + "session_id": session_id, + "is_multi_turn": True, + "turn_count": len(turns), + "turn_responses": all_responses, + } + ) + traces.append(trace) + + else: + # Handle single-turn conversation (original logic) + query = test_case["customer_query"] + + # Augment query with customer ID if available + if customer_id and f"customer {customer_id}" not in query.lower(): + query = f"I'm customer {customer_id}. {query}" + + print(f"[{i}/{len(test_cases)}] {test_id}") + print(f"Query: {query[:80]}...") + + # Use unique session ID to avoid cached conversation context + session_id = f"{agent_name}_eval_{test_id}_{uuid.uuid4().hex[:8]}" + + try: + import httpx + + request_data = { + "prompt": query, + "session_id": session_id + } + + async with httpx.AsyncClient() as client: + response_obj = await client.post( + f"{backend_url}/chat", + json=request_data, + timeout=60.0 + ) + response_obj.raise_for_status() + + result = response_obj.json() + response = result.get("response", "") + tools_used = result.get("tools_used", []) + + # Handle both old format (list of strings) and new format (list of dicts) + tool_calls = [] + for t in (tools_used or []): + if isinstance(t, dict): + tool_calls.append(t) + else: + tool_calls.append({"name": t, "args": {}}) + + print(f" ✓ Response: {response[:100]}...") + print(f" ✓ Tools called: {len(tool_calls)}") + + trace = AgentTrace( + query=test_case["customer_query"], + response=response, + tool_calls=tool_calls, + metadata={ + "test_id": test_id, + "agent_backend": backend_url, + "session_id": session_id, + "augmented_query": query, + "is_multi_turn": False, + } + ) + traces.append(trace) + + except Exception as e: + print(f" ❌ Error: {e}") + trace = AgentTrace( + query=query, + response=f"Error: {str(e)}", + tool_calls=[], + metadata={ + "test_id": test_id, + "agent_backend": backend_url, + "error": str(e), + "is_multi_turn": False, + } + ) + traces.append(trace) + + print() + + # 6. Generate evaluation_input_data.jsonl for Foundry integration + print(f"{'=' * 80}") + print(f"GENERATING FOUNDRY DATA FILE") + print(f"{'=' * 80}\n") + + foundry_data_file = Path(__file__).parent / "evaluation_input_data.jsonl" + with open(foundry_data_file, 'w') as f: + for trace in traces: + # Extract test case data from metadata + test_id = trace.metadata.get("test_id", "unknown") + + # Find matching test case from original dataset + matching_test = None + for test_case in test_cases: + if test_case.get("id") == test_id: + matching_test = test_case + break + + # Prepare data in format expected by run_eval.py + foundry_row = { + "query": trace.query, + "response": trace.response, + "expected_tools": matching_test.get("expected_tools", []) if matching_test else [], + "required_tools": matching_test.get("required_tools", []) if matching_test else [], + "success_criteria": matching_test.get("success_criteria", {}) if matching_test else {}, + "tool_calls": [{"name": tc["name"], "args": tc.get("args", {})} for tc in trace.tool_calls] + } + + f.write(json.dumps(foundry_row) + '\n') + + print(f"✓ Generated {foundry_data_file} with {len(traces)} evaluation rows") + + # 7. Run local evaluation (if --local or neither flag specified) + if run_local: + print(f"{'=' * 80}") + print(f"EVALUATING RESULTS (LOCAL)") + print(f"{'=' * 80}\n") + + runner = AgentEvaluationRunner(dataset_path=str(dataset_path)) + summary = runner.run_evaluation( + traces, + output_dir=str(Path(__file__).parent / "eval_results") + ) + + # Display summary + print(f"\n{'=' * 80}") + print(f"EVALUATION SUMMARY - {backend_url}") + print(f"{'=' * 80}") + print(f"Agent: {agent_name}") + print(f"Total Tests: {summary['total_tests']}") + print(f"Passed: {summary['passed']} ✓") + print(f"Failed: {summary['failed']} ✗") + print(f"Pass Rate: {summary['pass_rate']:.1%}") + print(f"Average Score: {summary['average_score']:.2f}") + + # Show different metric emphasis for multi-turn vs single-turn + if args.multi_turn_only: + print(f"\n📊 Multi-Turn Metrics (outcome-focused, 1-5 scale, threshold: 3):") + outcome_metrics = ["solution_accuracy", "task_adherence", "intent_resolution", "coherence", "fluency", "relevance"] + for metric in outcome_metrics: + score = summary['metric_averages'].get(metric, 0) + bar = "█" * int(score * 4) + status = "✓" if score >= 3.0 else "✗" + print(f" {metric:30s}: {score:4.1f}/5 {bar:20} {status}") + else: + print(f"\nMetric Breakdown (1-5 scale, threshold: 3):") + for metric, score in summary['metric_averages'].items(): + bar = "█" * int(score * 4) # Scale bar for 1-5 range (max 20 chars at score 5) + status = "✓" if score >= 3.0 else "✗" + print(f" {metric:30s}: {score:4.1f}/5 {bar:20} {status}") + + print(f"\n{'=' * 80}") + print(f"✓ Local evaluation complete! Check eval_results/ for detailed reports.") + print(f"{'=' * 80}\n") + + # 8. Push to Azure AI Foundry if --remote flag is set + if run_remote: + print(f"{'=' * 80}") + print(f"PUSHING RESULTS TO AZURE AI FOUNDRY") + print(f"{'=' * 80}\n") + + # Check for required environment variable + project_endpoint = os.environ.get("AZURE_AI_PROJECT_ENDPOINT") + + if not project_endpoint: + print("❌ Missing AZURE_AI_PROJECT_ENDPOINT in .env file") + print(" Get this from: Azure AI Foundry → Your Project → Settings → Project details") + print(" Example: https://your-account.services.ai.azure.com/api/projects/your-project") + print("\n Skipping remote evaluation...") + else: + # Determine eval type for naming + if args.multi_turn_only: + eval_type = "multi-turn" + elif args.single_turn_only: + eval_type = "single-turn" + else: + eval_type = "mixed" + + # Use the new Azure AI Projects SDK approach (azure-ai-projects>=2.0.0b1) + # This uses openai_client.evals API instead of azure.ai.evaluation.evaluate() + await run_foundry_evaluation(traces, foundry_data_file, agent_name, test_cases, eval_type) + + # Give async tasks time to cleanup + await asyncio.sleep(0.1) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n\nEvaluation cancelled by user.") + finally: + # Ensure all async resources are cleaned up + pass diff --git a/agentic_ai/evaluations/telemetry.py b/agentic_ai/evaluations/telemetry.py new file mode 100644 index 000000000..166fe5536 --- /dev/null +++ b/agentic_ai/evaluations/telemetry.py @@ -0,0 +1,61 @@ +import os +from typing import Optional + +try: # Optional dependency; backend should still run without it + from azure.monitor.opentelemetry import configure_azure_monitor + from agent_framework.observability import setup_observability + from opentelemetry.sdk.resources import Resource +except Exception: # pragma: no cover - best-effort import + configure_azure_monitor = None # type: ignore[assignment] + setup_observability = None # type: ignore[assignment] + Resource = None # type: ignore[assignment] + + +def setup_telemetry() -> None: + """Configure Azure Monitor for Agent Framework to show traces in Foundry. + + This follows the exact pattern from the Microsoft blog post: + "Agentic Applications on Azure Container Apps with Microsoft Foundry" + """ + + print("🔍 DEBUG: setup_telemetry() called") + + connection_string: Optional[str] = os.getenv("APPLICATION_INSIGHTS_CONNECTION_STRING") + print(f"🔍 DEBUG: Application Insights connection string exists: {bool(connection_string)}") + + if not connection_string: + print("❌ DEBUG: No APPLICATION_INSIGHTS_CONNECTION_STRING found, skipping telemetry") + return + + if configure_azure_monitor is None: + print("❌ DEBUG: configure_azure_monitor not available, skipping telemetry") + return + + try: + print("🚀 DEBUG: Calling configure_azure_monitor...") + # Configure Azure Monitor first (exact pattern from blog) + configure_azure_monitor( + resource=Resource.create({"service.name": "contoso-agent-backend"}) if Resource else None, + connection_string=connection_string, + disable_offline_storage=True, # Disable storage to avoid the NoneType error + ) + print("✅ DEBUG: configure_azure_monitor completed") + + # Enable Microsoft Agent Framework telemetry (correct function!) + if setup_observability is not None: + print("🚀 DEBUG: Calling setup_observability...") + setup_observability(enable_sensitive_data=False) + print("✅ DEBUG: setup_observability completed") + else: + print("❌ DEBUG: setup_observability not available") + + print("🎉 DEBUG: Telemetry setup complete!") + + except Exception as e: + print(f"❌ DEBUG: Telemetry setup failed with error: {e}") + import traceback + traceback.print_exc() + + except Exception: + # Telemetry should never break the app; swallow configuration errors. + return \ No newline at end of file From f2ba84753955be5c5d0662b23394a45d03eb68c9 Mon Sep 17 00:00:00 2001 From: "James N." Date: Tue, 3 Feb 2026 13:12:46 -0800 Subject: [PATCH 9/9] update CI/CD workflow --- .github/workflows/agent-evaluation.yml | 190 + mcp/data/contoso.db | Bin 1445888 -> 1445888 bytes tests/evaluation/.env.template | 56 - tests/evaluation/README.md | 300 -- tests/evaluation/__init__.py | 2 - .../evaluation/agent_comparison_results.json | 1611 --------- tests/evaluation/agent_evaluator.py | 658 ---- tests/evaluation/agent_runner.py | 376 -- tests/evaluation/all_agents_comparison.json | 1 - tests/evaluation/llm_judge_evaluator.py | 759 ---- tests/evaluation/pyproject.toml | 42 - tests/evaluation/test_agent_comparison.py | 484 --- tests/evaluation/test_data.jsonl | 10 - tests/evaluation/test_scenario_evaluation.py | 2020 ----------- tests/evaluation/uv.lock | 3198 ----------------- tests/test_agent_evaluation.py | 512 --- 16 files changed, 190 insertions(+), 10029 deletions(-) create mode 100644 .github/workflows/agent-evaluation.yml delete mode 100644 tests/evaluation/.env.template delete mode 100644 tests/evaluation/README.md delete mode 100644 tests/evaluation/__init__.py delete mode 100644 tests/evaluation/agent_comparison_results.json delete mode 100644 tests/evaluation/agent_evaluator.py delete mode 100644 tests/evaluation/agent_runner.py delete mode 100644 tests/evaluation/all_agents_comparison.json delete mode 100644 tests/evaluation/llm_judge_evaluator.py delete mode 100644 tests/evaluation/pyproject.toml delete mode 100644 tests/evaluation/test_agent_comparison.py delete mode 100644 tests/evaluation/test_data.jsonl delete mode 100644 tests/evaluation/test_scenario_evaluation.py delete mode 100644 tests/evaluation/uv.lock delete mode 100644 tests/test_agent_evaluation.py diff --git a/.github/workflows/agent-evaluation.yml b/.github/workflows/agent-evaluation.yml new file mode 100644 index 000000000..34b554cbc --- /dev/null +++ b/.github/workflows/agent-evaluation.yml @@ -0,0 +1,190 @@ +name: Agent Evaluation + +on: + # Run on PR when agent/evaluation code changes + pull_request: + paths: + - 'agentic_ai/agents/**' + - 'agentic_ai/evaluations/**' + + # Allow manual trigger + workflow_dispatch: + inputs: + environment: + description: Target environment + type: choice + options: [dev, integration] + default: dev + agent_name: + description: 'Agent name for evaluation tracking' + type: string + default: 'ci-agent' + limit: + description: 'Limit number of test cases (0 = all)' + type: number + default: 5 + eval_type: + description: 'Evaluation type' + type: choice + options: [all, single-turn-only, multi-turn-only] + default: all + push_to_foundry: + description: 'Push results to Azure AI Foundry' + type: boolean + default: false + + # Callable from other workflows + workflow_call: + inputs: + environment: + type: string + required: false + default: 'dev' + backend_endpoint: + type: string + required: true + description: 'Backend API endpoint URL' + mcp_endpoint: + type: string + required: true + description: 'MCP service endpoint URL' + agent_name: + type: string + required: false + default: 'ci-agent' + limit: + type: number + required: false + default: 0 + push_to_foundry: + type: boolean + required: false + default: false + +env: + PYTHON_VERSION: '3.12' + +jobs: + # ============================================================================ + # Evaluation - Run agent evaluation against test scenarios + # ============================================================================ + evaluate: + name: Agent Evaluation + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # For OIDC authentication + + environment: ${{ inputs.environment || 'dev' }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install uv + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Install dependencies + run: | + cd agentic_ai/applications + uv sync + + - name: Azure Login (OIDC) + if: ${{ inputs.push_to_foundry == true }} + uses: azure/login@v2 + with: + client-id: ${{ vars.AZURE_CLIENT_ID }} + tenant-id: ${{ vars.AZURE_TENANT_ID }} + subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Get Azure credentials from Key Vault + if: ${{ inputs.push_to_foundry == true }} + run: | + KEYVAULT_NAME="${{ vars.KEYVAULT_NAME }}" + + if [ -n "$KEYVAULT_NAME" ]; then + AOAI_KEY=$(az keyvault secret show --vault-name "$KEYVAULT_NAME" --name "aoai-key" --query value -o tsv 2>/dev/null || echo "") + echo "::add-mask::$AOAI_KEY" + echo "AZURE_OPENAI_API_KEY=$AOAI_KEY" >> $GITHUB_ENV + + AI_PROJECT_ENDPOINT=$(az keyvault secret show --vault-name "$KEYVAULT_NAME" --name "ai-project-endpoint" --query value -o tsv 2>/dev/null || echo "") + echo "AZURE_AI_PROJECT_ENDPOINT=$AI_PROJECT_ENDPOINT" >> $GITHUB_ENV + fi + + - name: Run Agent Evaluation + run: | + cd agentic_ai/applications + + # Build command + CMD="uv run python ../evaluations/run_agent_eval.py" + CMD="$CMD --agent ${{ inputs.agent_name || 'ci-agent' }}" + CMD="$CMD --backend-url ${{ inputs.backend_endpoint || 'http://localhost:7000' }}" + + # Add limit if specified + if [ "${{ inputs.limit }}" != "0" ] && [ -n "${{ inputs.limit }}" ]; then + CMD="$CMD --limit ${{ inputs.limit }}" + fi + + # Add eval type filter + if [ "${{ inputs.eval_type }}" == "single-turn-only" ]; then + CMD="$CMD --single-turn-only" + elif [ "${{ inputs.eval_type }}" == "multi-turn-only" ]; then + CMD="$CMD --multi-turn-only" + fi + + # Add remote flag if pushing to Foundry + if [ "${{ inputs.push_to_foundry }}" == "true" ]; then + CMD="$CMD --remote" + else + CMD="$CMD --local" + fi + + echo "Running: $CMD" + $CMD + env: + AZURE_OPENAI_ENDPOINT: ${{ vars.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_CHAT_DEPLOYMENT: ${{ vars.AZURE_OPENAI_DEPLOYMENT }} + AZURE_OPENAI_API_VERSION: '2025-03-01-preview' + MCP_SERVER_URI: ${{ inputs.mcp_endpoint || 'http://localhost:8000/mcp' }} + + - name: Upload evaluation results + uses: actions/upload-artifact@v4 + if: always() + with: + name: evaluation-results + path: | + agentic_ai/evaluations/eval_results/ + agentic_ai/evaluations/evaluation_input_data.jsonl + retention-days: 30 + + - name: Generate Summary + if: always() + run: | + echo "## 📊 Agent Evaluation Results" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Agent | ${{ inputs.agent_name || 'ci-agent' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Environment | ${{ inputs.environment || 'dev' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Eval Type | ${{ inputs.eval_type || 'all' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Test Limit | ${{ inputs.limit || 'all' }} |" >> $GITHUB_STEP_SUMMARY + echo "| Push to Foundry | ${{ inputs.push_to_foundry || 'false' }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Metrics Evaluated" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Single-Turn (tool-focused):**" >> $GITHUB_STEP_SUMMARY + echo "- Tool behavior (recall, precision, efficiency)" >> $GITHUB_STEP_SUMMARY + echo "- Completeness, response quality, grounded accuracy" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Multi-Turn (outcome-focused):**" >> $GITHUB_STEP_SUMMARY + echo "- Solution accuracy, task adherence, intent resolution" >> $GITHUB_STEP_SUMMARY + echo "- Coherence, fluency, relevance" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "📁 See artifacts for detailed results" >> $GITHUB_STEP_SUMMARY diff --git a/mcp/data/contoso.db b/mcp/data/contoso.db index d52222bb1b469557dea3ef28cd353d4135f066dc..e5397adf1ae2fccc93d0352c726b24c3d7c7e74a 100644 GIT binary patch delta 6785 zcmd5=Yit}>6`t8$?|RpFck*m&StZvAZIU*2_UXsC&C7Y2)JYsSC?&0u@y_*nn%$Yr zJnXE5Ivtm7ASsYGNnM1HLL*W2M@6a#EEFLGLJ+7__))0>m8z|TDxg(CP^d~3aqi5{ zcz4II7JeXQweigT?!D)n?|k>W`}mb(k6$@}Z9`y2Nj_b&Gi_r~>=7k$K>f4bh8d_Oj`qa_eBS1^~uK{l|0 zx#WCZq+bcUPD@z>+0x!dD^h=;jjiI2F~61ULvGwV;X??zulPoX{Ne)Jaz z_=Eq)fM+4#ql4_VnaDME>j~ys?02#6#vYBOqaR0q7Cj$58ojgikF77Y8m-$~Baz=k zo{Nk{wuJu?{&Bb(J`}#Kq31#;L-z##9=s8JBKU>is=)69&j(6@ zo&dxCioM7_!fs?Pt~}ha{r)=;!?K;7>!O_rjp6wv9h*icJ>;(A$E4v4R&_bjlhSyTW$j?@2jF<6@{v(rHaB;6se-t zDvDN7tcu#I2#OZ}0ZM#yi0_D=W4K>)m$+SAOX6nYeBxkYW&E}HO#DmnJJDO{DjG+d z+um#Yew*5si+vQk5qm7QGZu>eGI|c`_dwJf7z#6iu1ptmOUUOHL)K3ia#6{jA|mX) zrz@F;e>k1w(>;7HeYExpPf7ybd3b1G|MI}8ET8W8f%gH}Tx#i5KcC9+$&?Shck!iD zU8xKPaI%lj_V~biT(CKR)D9Y;DL&KdL+^Ici-S`=d?w=q4>o{JU&5WGd--gZPx?2$ zs{y?*ILT*{zNL5i!DfA}ou#4B{e0G6t{r~#0^qctDBBkRn>CtwOXv8Mf9Y)tpqs&2 zKIbRO{fmLkuB5wkHpgfD8^3QcbR9U&C$qkU4=f2bfuxiseAdqteo3?gPV!klNBfrt zHleT6ewy_y51r1;2ku=SIMoNDVES^_vrKdjXzBy!mIqFz_-vmIHbLk-HOnp&4GGWr zmd-2(oP%{i&*s3xH6cwe6Ae-O`ucDRz?(D2nOid>uSRY^n?I3&4+ozW@L36;RkQgM ztH1NtHE}RW;TX>({v7`}-iP+L?TGPk4BQYkWtg>Vk53N?TRMfRhfB(}q+o5m_HJG+imd zle{92OKJ(m=Y_E%(XgNrY-%KLz_%caSdwu;QQ);Ai;^xWayRzCnzNYJxX6T{ic@r& zl2E~8#NCK!5UeY79$k_RSWuGf!&lc~KUkDBcw2a%8kf_&fK*aeR9!Q1EnicDMj>1k8=gM3oe=8$TeOA}|5n6ugp3{X%aG{ttlWzxLQ=HOwR2^*#Z=PH6lRVizjOcJ$% z$H^21(*%stwg7DmO_B?ETm?$0g2RweqE1SAf`LUUEDdp%1eNx-tf(cSXlIL@p{XEp zo}wigk~}6Af!q+VNo#LyvsTfF?}Z*{-(wwWPb3~B`3YId!vsUL5{+$;25Ztw%Rfv+ z$tVp{I^!|r4Af{$QGjy=tOB8xQnBI-+2;P-U{i!9!Lcz0}g$mXe;APAYixg$n;(9aNYQ1aKT1SgD>l&5tw!-7nP~Gnn70+ zQNxOiQ~SWB;&M@t{b4AwCTX;vAmbXv*I**i%92j%u-(|L5hPwWG|J8?4R55Vjb=AK zTqJ@96!fAZxG2d4pEd+qBwHHFsxm2QkPpbM+pw#w6*S@;6|RwK!H#%{Hag|gVWN&x z0)Y2!tX(P%hpyYYqr?Q7(dMWRVe!ZnxuDPr+X#>7P0^Qy3LOJma1!CmflyS5AXe~5 zjjhoAbix7PaZN61vOpw(xo#lP4WtMPXyhj_j0KM96M~E>yQvJ<^6~=v+chVjEpwrJ zRx_8G#P{MQ^h3A_oQ_?Hz8URn%}1_={}t|UnGU@b{2;g^u$z69xqPuRSum{;i=$o8 zLAzk{bZ2`kwo58PDGE&)O!2uo_V1Bx`tLM!1){DaicMQ8O3oXq$RN?g9uBHBS^@f$ z@FWyjq)av;5>PI7klYXM@5W zwA*p6J*XJy~m9cqdyQfZPabLTsTk_n=7V1r!N$_1x8oalC%G%9hVq(G&&s$|@N z=gK%1%Fxqiq!QE=+7Gs3x_j9)ZDd#wI? z)vjYKjlY*qdp*C9TANGAwM3(6miNXemyqa^)#fqrIP_Pe(=wesJC~9^xX*yYc6DkY zCfcO>3}l?|cr}3pjF*N*5|C4TOv4E zJNzYb!_D@aeA>2D|NWU-f220BaNZuuSW&x&Ok4IW(ky%K4l^lGhq*)lv&kN9v}T>c zR60unI3W=`A_qQJf**_q;OMoF#zw7ko1eE@Jvt3rLrrvQWK%i^SFQ#&l_bXvdwONg z36>`THIq8$4d%w!Hf``Nr6`|X&d9DrZ<^rb6%|BR{(ENBYH3`0CPt`&60rF)|^Gd zQQv6JeCDFwV7Y7=v0B`8gIHGTJJGSY4f@kd^(UEfV^~Ntmyk>dm%3QE1T}EpkYck&8Tf6j2QK0BM`eCGe zJ=`+s4>#c@>2LQAu)W^sxyGR8qa71XEu^HtRYQV1pXl63y*rb4gRrs|>qgk#b=G`k M&8+##T5I=z0E_l-tN;K2 delta 499 zcmYMwJ!n%=7{>AQ-tT*J&UsB5NkdV&6=~|AiCRTOOJnI$im5Jv7Lw9QQOOjj!=}1RVD-XJA>TM&YMZNhV+TOLL0u zFJ6ZMxjPQ19xT!YfN{d68$GBp8P|KBorXD)*D%^Dof zJ1fpZ=c;qU{$szj@7P!D1J;H$Z{4!aTc-KN^v%3^)c9q*GK4W=#Pn7DsqX1#bw&HA zRkgg9PpL!vPopcO}cWk28W%{=}t-jTsLmV{YDMq zes{v|fM_Yw02yg0yMuZx$TlRXDySx?E~p`>DX1l=EvO@CNKh&$drtY@B}c_H!4G&J z(?Q!)=wwm`QF>2~k+whj*^HGYb3A~7eSvTwA}4bl54N7bQnXMlpvLu(NBCoQgD%qE TkXLt;xlqY#My1!>e{b*~aK)A( diff --git a/tests/evaluation/.env.template b/tests/evaluation/.env.template deleted file mode 100644 index d5dcfc766..000000000 --- a/tests/evaluation/.env.template +++ /dev/null @@ -1,56 +0,0 @@ -# Agent Evaluation Environment Configuration -# Copy this file to .env and fill in your Azure OpenAI credentials - -# ═══════════════════════════════════════════════════════════════════════════════ -# AZURE OPENAI CONFIGURATION (Required) -# ═══════════════════════════════════════════════════════════════════════════════ -AZURE_OPENAI_ENDPOINT="https://your-endpoint.openai.azure.com" -AZURE_OPENAI_KEY="your-api-key-here" -AZURE_OPENAI_API_KEY="your-api-key-here" # Alias for compatibility -AZURE_OPENAI_CHAT_DEPLOYMENT="gpt-4.1" -AZURE_OPENAI_DEPLOYMENT="gpt-4.1" # For LLM-as-judge evaluators -AZURE_OPENAI_API_VERSION="2025-03-01-preview" - -# ═══════════════════════════════════════════════════════════════════════════════ -# LLM-AS-JUDGE CONFIGURATION (For AI-assisted evaluation) -# ═══════════════════════════════════════════════════════════════════════════════ -# Model deployment for LLM judge evaluators (recommend gpt-4o or better) -# Set USE_REASONING_MODEL=true if using o-series models (o1, o3-mini) -LLM_JUDGE_DEPLOYMENT="gpt-4o" -USE_REASONING_MODEL="false" - -# ═══════════════════════════════════════════════════════════════════════════════ -# MCP SERVER CONFIGURATION (Required for integration tests) -# ═══════════════════════════════════════════════════════════════════════════════ -MCP_SERVER_URI="http://localhost:8000/mcp" - -# ═══════════════════════════════════════════════════════════════════════════════ -# AGENT MODULES TO EVALUATE -# ═══════════════════════════════════════════════════════════════════════════════ -# Single Agent - Basic intelligent agent with MCP tools -SINGLE_AGENT_MODULE="agents.agent_framework.single_agent" - -# Reflection Agent - Primary + Reviewer pattern for quality assurance -REFLECTION_AGENT_MODULE="agents.agent_framework.multi_agent.reflection_agent" - -# Default agent for single-agent tests -DEFAULT_AGENT_MODULE="agents.agent_framework.single_agent" - -# ═══════════════════════════════════════════════════════════════════════════════ -# EVALUATION SETTINGS -# ═══════════════════════════════════════════════════════════════════════════════ -# Number of test cases to run in quick mode (default: 3) -EVAL_QUICK_TEST_COUNT=3 - -# Enable LLM-as-judge evaluation (uses additional Azure OpenAI calls) -# When true, uses Azure AI Foundry evaluators (IntentResolution, TaskAdherence, etc.) -# When false, uses simple keyword matching for outcome evaluation -EVAL_USE_LLM_JUDGE="true" - -# Tool call accuracy threshold (0.0 - 1.0) -EVAL_TOOL_ACCURACY_THRESHOLD=0.5 - -# ═══════════════════════════════════════════════════════════════════════════════ -# OPTIONAL: Embedding model for AI evaluation -# ═══════════════════════════════════════════════════════════════════════════════ -AZURE_OPENAI_EMBEDDING_DEPLOYMENT="text-embedding-ada-002" diff --git a/tests/evaluation/README.md b/tests/evaluation/README.md deleted file mode 100644 index 1ab1867fb..000000000 --- a/tests/evaluation/README.md +++ /dev/null @@ -1,300 +0,0 @@ -# Agent Evaluation Framework - -This directory contains a comprehensive evaluation framework for AI agents using the **Azure AI Foundry Evaluation SDK** with **LLM-as-Judge** capabilities. - -## 📋 Overview - -The evaluation framework tests agent performance across multiple dimensions: - -### Evaluation Types - -| Type | Description | -|------|-------------| -| **Process-Based** | Evaluates HOW the agent works (tool calls, reasoning steps) | -| **Goal-Based** | Evaluates WHAT the agent achieves (outcome quality) | - -### LLM-as-Judge Evaluators (Azure AI Foundry) - -| Evaluator | Type | Description | -|-----------|------|-------------| -| `IntentResolutionEvaluator` | Goal | Did the agent correctly identify user intent? | -| `TaskAdherenceEvaluator` | Goal | Did the response follow the assigned task? | -| `ToolCallAccuracyEvaluator` | Process | Were the correct tools called? | -| `CoherenceEvaluator` | Quality | Is the response logically coherent? | -| `FluencyEvaluator` | Quality | Is the language natural? | -| `RelevanceEvaluator` | Quality | Is the response relevant? | - -### Fallback Metrics (No LLM Required) - -| Metric | Description | -|--------|-------------| -| **Tool Recall/Precision/F1** | Rule-based tool call accuracy | -| **Keyword Coverage** | Simple keyword matching for outcomes | - -## 🆕 Standalone Setup (Recommended) - -The evaluation module runs **independently** from the applications folder. - -### Prerequisites - -1. **MCP Server** running at `http://localhost:8000/mcp` -2. **Azure OpenAI** credentials (gpt-4.1 or gpt-4o recommended for LLM judges) -3. **uv** package manager - -### Quick Start - -```bash -# Navigate to evaluation folder -cd tests/evaluation - -# Install dependencies (first time only) -$env:UV_LINK_MODE="copy" # Windows only, for OneDrive compatibility -uv sync - -# Run quick comparison (3 test cases, ~1 min) -uv run pytest test_agent_comparison.py::TestAgentComparison::test_quick_comparison -v -s - -# Run full comparison with LLM judges -uv run pytest test_scenario_evaluation.py::TestAgentComparison -v -s - -# Test LLM judge directly -uv run python llm_judge_evaluator.py -``` - -### Configuration - -Edit `.env` file in `tests/evaluation/` to configure: - -```bash -# Azure OpenAI for agents -AZURE_OPENAI_ENDPOINT=https://your-endpoint.openai.azure.com -AZURE_OPENAI_API_KEY=your-api-key -AZURE_OPENAI_CHAT_DEPLOYMENT=gpt-5.2-chat - -# LLM Judge (use gpt-4.1 for compatibility with azure-ai-evaluation SDK) -AZURE_OPENAI_DEPLOYMENT=gpt-4.1 -LLM_JUDGE_DEPLOYMENT=gpt-4.1 -USE_REASONING_MODEL=false # Set true for o-series models - -# MCP Server -MCP_SERVER_URI=http://localhost:8000/mcp - -# Evaluation settings -EVAL_USE_LLM_JUDGE=true -EVAL_QUICK_TEST_COUNT=3 -``` - ---- - -## 🧠 LLM-as-Judge Feature - -Instead of simple keyword matching, the framework uses Azure AI Foundry's LLM-based evaluators: - -```python -from llm_judge_evaluator import LLMJudgeEvaluator, ToolCall, ToolDefinition - -evaluator = LLMJudgeEvaluator() - -result = await evaluator.evaluate( - query="What is my invoice total?", - response="Your invoice total is $542.50...", - tool_calls=[ToolCall(name="get_customer_invoices", arguments={"customer_id": 123})], - tool_definitions=[ToolDefinition(name="get_customer_invoices", description="Get invoices")] -) - -print(f"Intent Resolution: {result.intent_resolution_score}/5 - {result.intent_resolution_result}") -print(f"Task Adherence: {result.task_adherence_score}/5 - {result.task_adherence_result}") -print(f"Tool Accuracy: {result.tool_call_accuracy_score}/5 - {result.tool_call_accuracy_result}") -``` - -### Multi-Turn Support - -Azure AI Foundry evaluators support full conversation history: - -```python -from llm_judge_evaluator import ConversationMessage - -conversation = [ - ConversationMessage(role="user", content="What's my account status?"), - ConversationMessage(role="assistant", content="Let me check...", tool_calls=[...]), - ConversationMessage(role="tool", content='{"status": "active"}', tool_call_id="call_1"), - ConversationMessage(role="assistant", content="Your account is active."), -] - -result = await evaluator.evaluate( - query="What's my account status?", - response="Your account is active.", - conversation=conversation, - system_prompt="You are a helpful customer service agent." -) -``` - ---- - -## 🔄 Agent Comparison - -The framework compares **single_agent** vs **reflection_agent**: - -| Agent | Description | Expected Performance | -|-------|-------------|---------------------| -| **single_agent** | Direct LLM response | Faster, baseline quality | -| **reflection_agent** | Primary + Reviewer pattern | Slower, higher quality | - -### Sample Output - -``` -══════════════════════════════════════════════════════════════════════ -AGENT COMPARISON REPORT: single_agent vs reflection_agent -══════════════════════════════════════════════════════════════════════ -Test Cases: 3 - -Metric single_agent reflection_agent Diff ----------------------------------------------------------------------- -avg_execution_time 6.043 12.338 +6.295 -avg_response_length 936.667 1127.667 +191.000 -success_rate 1.000 1.000 0.000 ----------------------------------------------------------------------- -``` - ---- - -## 🚀 Legacy Quick Start (from applications folder) - -### Run Unit Tests (No external dependencies) - -```bash -# From the applications folder (recommended for uv) -cd agentic_ai/applications -uv run python -m pytest ../../tests/test_agent_evaluation.py -v -m "unit" -``` - -### Run Integration Tests (Requires MCP server + Azure OpenAI) - -1. **Start MCP server:** -```bash -cd mcp && uv run python mcp_service.py -``` - -2. **Start Backend:** -```bash -cd agentic_ai/applications && uv run python backend.py -``` - -3. **Run evaluation tests:** -```bash -cd agentic_ai/applications -uv run python -m pytest ../../tests/test_agent_evaluation.py -v -m "evaluation and integration" -``` - -### Run Full Evaluation Pipeline - -```bash -cd agentic_ai/applications - -# Run with AI-assisted evaluation -uv run python -m tests.evaluation.agent_evaluator \ - --test-data ../../tests/evaluation/test_data.jsonl \ - --agent-module agents.agent_framework.single_agent \ - --output ../../tests/evaluation/results.json - -# Run without AI evaluation (faster, no extra API costs) -uv run python -m tests.evaluation.agent_evaluator \ - --test-data ../../tests/evaluation/test_data.jsonl \ - --no-ai-eval -``` - -## 📁 Files - -| File | Description | -|------|-------------| -| `llm_judge_evaluator.py` | **NEW** LLM-as-Judge evaluator using Azure AI Foundry SDK | -| `agent_runner.py` | Generic agent test runner that works with any agent | -| `test_scenario_evaluation.py` | Scenario-based evaluation with dual metrics | -| `test_agent_comparison.py` | Agent comparison tests (single vs reflection) | -| `agent_evaluator.py` | Core evaluation module with AgentRunner and AgentEvaluator | -| `test_data.jsonl` | Test dataset with Contoso Communications scenarios | -| `pyproject.toml` | Standalone dependencies | -| `.env` | Environment configuration | - -## 📊 Test Data Format - -Test cases are stored in JSONL format: - -```json -{ - "query": "What's my billing summary?", - "customer_id": "251", - "expected_intent": "billing_inquiry", - "expected_tools": ["get_billing_summary", "get_customer_detail"], - "ground_truth": "The agent should retrieve and present the customer's billing summary.", - "category": "billing", - "complexity": "low" -} -``` - -### Categories Covered - -- **billing** - Invoice, payment, and balance queries -- **technical_support** - Service issues, data usage, connectivity -- **products** - Plan upgrades, international roaming -- **security** - Account lockout, authentication issues -- **promotions** - Discounts, loyalty rewards -- **support** - Support tickets, order returns - -## 🎯 Evaluation Thresholds - -Default thresholds (configurable in `EvaluationThresholds`): - -| Metric | Threshold | Description | -|--------|-----------|-------------| -| Tool Call Accuracy | 0.5 | F1 score for tool calls (lower to account for agent using tool subsets) | -| Groundedness | 0.7 | Normalized score (1-5 scale) | -| Relevance | 0.8 | Normalized score (1-5 scale) | -| Coherence | 0.8 | Normalized score (1-5 scale) | -| Fluency | 0.8 | Normalized score (1-5 scale) | - -## 🔧 CI/CD Integration - -The evaluation runs automatically in CI/CD: - -1. **On PR** (changes to `agentic_ai/agents/**`): Unit tests only -2. **On workflow_call**: Full integration tests against deployed services -3. **Manual trigger**: Optional full evaluation with AI metrics - -### GitHub Actions Workflow - -```yaml -# Trigger evaluation manually -gh workflow run agent-evaluation.yml \ - -f environment=dev \ - -f run_full_evaluation=true \ - -f include_ai_evaluation=true -``` - -## 📈 Sample Output - -``` -============================================================ -EVALUATION SUMMARY -============================================================ -Total Tests: 10 -Passed: 8 -Failed: 2 -Pass Rate: 80.0% -Avg Tool F1: 0.85 -Avg Exec Time: 1523ms - -By Category: - billing: 3/3 (100%) - technical_support: 2/2 (100%) - products: 1/2 (50%) - security: 1/1 (100%) - promotions: 1/2 (50%) -============================================================ -``` - -## 🔗 Related Documentation - -- [Azure AI Evaluation SDK](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/develop/agent-evaluate-sdk) -- [Contoso Communications Scenario](../../SCENARIO.md) -- [Microsoft Agent Framework](../agentic_ai/agents/agent_framework/README.md) diff --git a/tests/evaluation/__init__.py b/tests/evaluation/__init__.py deleted file mode 100644 index 9c57ca8fd..000000000 --- a/tests/evaluation/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Agent Evaluation Module -# Comprehensive evaluation framework for AI Agents using Azure AI Evaluation SDK diff --git a/tests/evaluation/agent_comparison_results.json b/tests/evaluation/agent_comparison_results.json deleted file mode 100644 index 0617c2d92..000000000 --- a/tests/evaluation/agent_comparison_results.json +++ /dev/null @@ -1,1611 +0,0 @@ -{ - "single": [ - { - "scenario": "billing_high_invoice", - "scenario_name": "Invoice Higher Than Usual", - "success": true, - "tool_recall": 0.4, - "tool_precision": 1.0, - "tool_f1": 0.5714285714285715, - "keyword_coverage": 0.8333333333333334, - "total_time": 10.795327425003052, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to understand why their last invoice was unusually high. The agent provided a clear breakdown, explained the overage charges, and offered next steps. The response is thorough, accurate, and directly resolves the user's intent.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The assistant provided a detailed breakdown of the invoice and explained why the charge was higher than usual, offering relevant next steps such as reviewing usage or upgrading the plan. However, the assistant referenced specific invoice details (date, overage charges, plan type, payments made) without any corroborating evidence from tool outputs. Since the TOOL_CALLS are empty and such details appear to be fabricated rather than verified, this constitutes a material failure in verification and alignment with allowed workflows. No safety or privacy rules were breached, but claiming external, user-specific data without verified access is a procedural failure.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response correctly identifies that the higher invoice is due to data overage charges and mentions the 10GB data cap, which addresses the root cause. However, it does not specify the exact amount of data used (22GB), the precise overage (12GB), or the per-GB overage rate ($7.50/GB), which are key details per the ground truth. The explanation is somewhat vague about the calculation of the extra charges and does not break down the $90 overage fee. While the agent offers to help upgrade the plan and review usage, it does not explicitly offer a one-time courtesy adjustment or set up data alerts, which are recommended solutions. Overall, the response is adequate but lacks the specific numbers and full solution options required for a higher score.", - "llm_eval_time": 34.66149950027466 - }, - { - "scenario": "billing_payment_history", - "scenario_name": "Payment History Inquiry", - "success": true, - "tool_recall": 0.5, - "tool_precision": 0.5, - "tool_f1": 0.5, - "keyword_coverage": 0.6, - "total_time": 10.674136638641357, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to see their recent payments and confirm if they all went through. The agent provided a detailed, accurate summary of each payment and its status, clearly indicating which were successful and which had issues, fully resolving the intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant presented a clear and detailed summary of recent payments for customer 5, explicitly indicating which payments went through successfully and which were incomplete or had issues. The information matches the user's request to review recent payments for confirmation. There are no indications of unauthorized actions, privacy breaches, or unsubstantiated claims since no external tool output was required or referenced, and the content is appropriate and relevant. The outcome is complete and fulfills the user's intent in a usable format without any material failures.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response fully meets the criteria for an excellent answer as outlined in the rubric. It provides a detailed payment history, listing each recent payment with invoice numbers, amounts, payment methods, and the status of each (successful, partial, or failed). The response clearly identifies any failed or pending payments and specifies the remaining balances for those invoices. It also confirms which payments were successful and offers helpful next steps, such as assistance with paying remaining balances, checking the total amount due, and setting up autopay. The agent's message is clear, accurate, and directly addresses the customer's needs, matching all key points in the ground truth solution.", - "llm_eval_time": 35.0008647441864 - }, - { - "scenario": "billing_autopay_setup", - "scenario_name": "Autopay Setup Request", - "success": true, - "tool_recall": 0.25, - "tool_precision": 0.5, - "tool_f1": 0.3333333333333333, - "keyword_coverage": 0.4, - "total_time": 19.29434609413147, - "tools_called": [ - "get_customer_detail", - "update_subscription", - "update_subscription", - "update_subscription" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted help setting up autopay for their bill. The agent attempted to enable autopay, explained the failure, and offered actionable next steps, including opening a support ticket. While autopay wasn't enabled directly, the agent provided a clear path to resolution.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant understood the user's request to set up autopay for their bill. It attempted to enable autopay but encountered a system validation error, which was properly disclosed. The assistant did not fabricate a successful result and provided alternative solutions, including opening a support ticket or checking for blocking issues, which are reasonable next steps given its current limits. There are no signs of privacy breaches, improper tool use, unauthorized actions, or violations of presentation or workflow. The response is relevant and maintains focus on the user's objective without making verifiable claims outside what tool interactions support.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response provides basic autopay information and attempts to check and enable autopay for the customer, which aligns with the \"Adequate\" level on the rubric. The agent identifies the root cause (a system validation error preventing autopay activation) and offers next steps, such as opening a support ticket or checking for outstanding balances. However, the response does not mention the $5 monthly autopay discount, does not explain how autopay works (e.g., auto-charge on due date), and does not confirm the payment method on file. While the agent does check the current status and offers to help further, it misses key details required for a higher score.", - "llm_eval_time": 23.657154321670532 - }, - { - "scenario": "billing_overdue_invoice", - "scenario_name": "Overdue Invoice Question", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 1.0, - "total_time": 9.471482276916504, - "tools_called": [ - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know the consequences of not paying an overdue invoice. The agent provided a clear, thorough explanation of progressive outcomes, including late fees, service suspension, delinquency, and collections, fully resolving the intent with relevant details and helpful options.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant provided a detailed and clear explanation of the typical consequences of not paying an overdue invoice, which matches the user's request for information. There is no indication of rule violations\u2014no sensitive information was exposed, no harmful content generated, and no unauthorized actions taken. The assistant did not attempt any real-world actions or claim external results that would require verification through tool interactions; its response was informational only. There is no evidence of workflow or sequencing errors, as all information was directly relevant to the user's inquiry. No material failures were identified across any axis.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response explains the consequences of overdue invoices in detail, including late fees, service suspension, delinquency, and possible collections, which aligns well with the ground truth's requirements for explaining consequences. It also offers solutions such as payment arrangements and invoice adjustments, and invites the customer to check specific overdue amounts and process payment, addressing the customer's needs and next steps. However, the response does not proactively list the specific overdue invoices, amounts, or due dates, which is a key element in the ground truth for a perfect score. The response is thorough and helpful but lacks the direct provision of account-specific details, making it \"Good\" but not \"Excellent\" per the rubric.", - "llm_eval_time": 25.370789289474487 - }, - { - "scenario": "billing_refund_request", - "scenario_name": "Refund Request for Service Issue", - "success": true, - "tool_recall": 0.5, - "tool_precision": 0.6666666666666666, - "tool_f1": 0.5714285714285715, - "keyword_coverage": 0.8, - "total_time": 11.742409467697144, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail", - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user requested a refund or credit for 3 days without internet. The agent explained eligibility, confirmed a service incident, and offered to submit a credit request, clearly outlining next steps. The response is thorough and actionable, fully resolving the intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant provided a clear and relevant response to the user's inquiry about a refund or credit for a three-day internet outage. It did not make unsupported claims, offered next steps for submitting a request, and did not take or claim real-world actions without evidence. No sensitive information was disclosed, and all content was appropriate and aligned with the user's intent. No required workflows or rule-based constraints were violated.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response acknowledges the service outage and offers an appropriate solution by proposing a pro-rated credit for the 3 days of disruption, which aligns with the ground truth. The agent verifies the outage by referencing a service incident on the customer's subscription and explains the need for a formal review per company policy. The response outlines next steps, including opening a billing/support ticket and clarifies when the credit would appear. However, it does not explicitly state that the credit will be applied or confirm its application\u2014approval is contingent on the review process, and the agent asks for the customer's confirmation to proceed. While thorough and customer-focused, the response falls slightly short of the \"Excellent\" rubric level, which requires direct application and confirmation of the credit.", - "llm_eval_time": 27.133212327957153 - }, - { - "scenario": "internet_slow", - "scenario_name": "Internet Slower Than Before", - "success": true, - "tool_recall": 0.25, - "tool_precision": 0.3333333333333333, - "tool_f1": 0.28571428571428575, - "keyword_coverage": 0.4, - "total_time": 12.916604042053223, - "tools_called": [ - "get_customer_detail", - "get_data_usage", - "create_support_ticket" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User reported slow internet despite paying for 1Gbps. Agent confirmed the issue, opened a high-priority support ticket, explained next steps, and offered further help. The response is thorough, proactive, and fully resolves the user's intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant correctly identified the user's issue (slow internet despite paying for 1Gbps), reviewed their service status and plan, and provided a clear summary of relevant account information. It took appropriate action by opening a high-priority support ticket and transparently shared the ticket details. Additionally, it provided reasonable next steps and troubleshooting suggestions, offering further assistance as needed. There are no fabricated claims of external actions, no privacy or safety violations, and no evidence of procedural errors. The absence of TOOL_CALLS aligns with the type of actions claimed, which are primarily internal support tasks. The output directly addresses the user's request and provides a usable, goal-oriented response.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response correctly identifies that the customer's service status is marked as \"slow,\" indicating a known performance issue, and acknowledges the frustration, which aligns with the ground truth's requirement to recognize the incident and apologize. The agent provides basic troubleshooting steps (restart router, test wired connection, check device usage) and proactively opens a high-priority support ticket, offering to monitor the ticket and assist further. However, the response does not mention the specific date the incident was reported (April 17) or explicitly state that there is an open service incident affecting the area, nor does it mention the possibility of a service credit once the issue is resolved. These omissions prevent it from achieving a perfect score, but overall, the response is thorough and customer-focused, meeting most of the rubric's criteria for a \"Good\" rating.", - "llm_eval_time": 23.770139694213867 - }, - { - "scenario": "internet_upgrade_inquiry", - "scenario_name": "Internet Speed Upgrade Options", - "success": true, - "tool_recall": 0.5, - "tool_precision": 1.0, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 0.6666666666666666, - "total_time": 11.915200471878052, - "tools_called": [ - "get_customer_detail", - "get_products" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted upgrade options for faster internet suitable for video calls. Agent provided a detailed review of the current plan, recommended higher speed and data tiers, and mentioned bundle options, fully addressing the intent with relevant, actionable choices and next steps.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant addressed the user's concern about slow internet for video calls, reviewed the current plan, and offered relevant upgrade options including higher speed, increased data cap, and a bundle. The assistant provided context for why upgrades would help and asked clarifying questions to tailor recommendations, aligning with the user's work-from-home scenario. There are no unauthorized actions, privacy issues, or procedural errors. The outcome is complete and usable for the user's goal.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response is good, as it shows the customer's current plan details (speed tier, data cap, status), presents upgrade options with relevant speed tiers (500 Mbps, 1 Gbps), and explains the benefits of upgrading in terms of speed and data cap. It also mentions the possibility of bundle plans and Gold loyalty promotions, which aligns with the rubric's recommendation to offer applicable promotions. The agent makes a recommendation based on the customer's work-from-home and video call needs, asking clarifying questions to further personalize the upgrade. However, the response does not provide specific pricing for the upgrade options, nor does it explicitly show the price difference from the current plan or mention benefits like a WiFi 6 router or priority support. These omissions prevent it from reaching the \"Excellent\" level (score 5), but it fulfills the requirements for a \"Good\" score (4) by presenting options, making a recommendation, and addressing the customer's needs.", - "llm_eval_time": 37.07470417022705 - }, - { - "scenario": "internet_router_reset", - "scenario_name": "Router Reset Help", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.8333333333333334, - "total_time": 10.020252227783203, - "tools_called": [ - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted clear instructions on how to reset their router. The agent provided detailed, step-by-step guidance for both soft and factory resets, included warnings, and offered further support options, fully resolving the intent with thoroughness and relevance.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant understood the user's problem (router not working) and provided step-by-step instructions for both a soft reset and a factory reset, clearly describing differences and consequences. There is no omission of essential information and no extraneous or distracting content. The instructions are accurate, safe, and do not expose sensitive information. No unauthorized or unverifiable external actions are claimed, and no tool usage is referenced beyond an offer to assist further. There are no procedural or presentation errors, and no violation of any explicit constraints. All evaluation dimensions are met.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response provides complete, step-by-step instructions for both a soft reset and a factory reset, clearly distinguishing between the two and warning that the factory reset will erase custom settings, which aligns with the factory settings warning in the rubric. The instructions include accurate details such as locating the reset button, using a paperclip, holding for 10\u201315 seconds, and waiting for the router to reboot, matching the ground truth solution. The agent also offers additional help by suggesting to check for outages, create a support ticket, or walk the customer through reconnecting devices, and offers to check the account or open a support ticket, which covers the rubric's requirement to offer further assistance. Overall, the response is thorough, accurate, and customer-focused, fully addressing the customer's needs.", - "llm_eval_time": 24.50334358215332 - }, - { - "scenario": "internet_outage_report", - "scenario_name": "Internet Outage Report", - "success": true, - "tool_recall": 0.5, - "tool_precision": 1.0, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 1.0, - "total_time": 11.861270427703857, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know if there was an internet outage in their area. The agent checked for outages, confirmed none were reported, explained possible causes, suggested troubleshooting, and offered to open a support ticket, thoroughly resolving the intent with clear next steps.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user asked about a possible internet outage in their area, and the assistant responded by checking and stating that there are no reported service incidents or outages for the user's area or service. Although TOOL_CALLS are empty (no actual tool verification of outage status), the assistant did not claim any real-world effects or unverifiable external data that contradicts what can be assumed from the input constraints\u2014it merely simulated an internal check, which is acceptable for responses when tool output isn't provided and no explicit tool requirement is stated. The assistant provided troubleshooting steps and offered to escalate with a support ticket, fully aligning with the user's goal and constraints given. There are no safety, privacy, or procedural failures apparent in the response.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response checks the customer's subscription status and confirms there are no reported outages, which aligns with the ground truth's first two key actions. It provides troubleshooting steps and offers to create a support ticket for further investigation, including a technician visit if needed. The response is empathetic and addresses the customer's needs by explaining possible causes and next steps. However, it does not mention offering a service credit for extended outages or proactively offering to notify the customer when service is restored, which are part of the ideal solution. Overall, the response is thorough and appropriate, but missing a couple of elements for a perfect score.", - "llm_eval_time": 34.99604535102844 - }, - { - "scenario": "internet_static_ip", - "scenario_name": "Static IP Request", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.6666666666666666, - "total_time": 9.186568260192871, - "tools_called": [ - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know if a static IP address is available for their home server. The agent clearly explained availability, requirements, and next steps, offering to check eligibility or initiate provisioning, fully resolving the intent with thorough and relevant information.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant addresses the user's request by confirming that static IP addresses are offered, provides relevant details about eligibility and requirements, and offers to help further by checking the user's subscription or initiating the process. The information is on topic and sufficient for the user's initial query, with next steps presented. There are no claims of completed external actions without supporting evidence and no privacy, safety, or procedural violations. The response is complete, relevant, and free of extraneous content.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response explains that static IPs are available, clarifies that they are typically offered with business or advanced residential plans, and notes that standard plans come with dynamic IPs. It also outlines the need to check the customer's current plan and offers to help with upgrades or provisioning a static IP. However, the response lacks specific details about which exact plans include static IPs and their pricing, as outlined in the ground truth solution. While the agent provides accurate general information and appropriate next steps, the absence of plan names and prices prevents it from being a perfect answer. Thus, it meets the \"Good\" criteria but falls short of \"Excellent.\"", - "llm_eval_time": 22.03885006904602 - }, - { - "scenario": "roaming_travel", - "scenario_name": "Travelling Abroad - Needs Roaming", - "success": true, - "tool_recall": 0.25, - "tool_precision": 0.5, - "tool_f1": 0.3333333333333333, - "keyword_coverage": 0.75, - "total_time": 11.262126922607422, - "tools_called": [ - "get_customer_detail", - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted information about international roaming for their upcoming trip to Spain. The agent provided detailed, relevant information about roaming status, options, timing, and next steps, fully addressing the user's needs and offering to enable roaming, thus excellently resolving the intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant provided a thorough overview of international roaming for the user's upcoming trip to Spain, clearly stating the user's current status, available options, and relevant timing concerns, and offered actionable next steps without making unverifiable claims or performing unauthorized actions. No sensitive data was exposed, no presentation rules were violated, and there were no tool actions incorrectly claimed. The response directly addressed the user's stated need for roaming information and supported follow-up requests, resulting in a usable and compliant output.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response fully meets the criteria for an excellent answer. It clearly identifies that international roaming is not enabled on the customer's account and acknowledges the urgency due to the 2-day timeline, referencing the recommended 3-day activation window. The agent confirms that Spain is covered under the roaming options and describes the available packages (voice, text, data, and extra data add-ons). The response offers to urgently enable roaming, recommend suitable packages, and set up usage alerts to prevent unexpected charges. All key facts and recommended actions from the ground truth are addressed, and the response is proactive, accurate, and customer-focused.", - "llm_eval_time": 24.91675329208374 - }, - { - "scenario": "mobile_data_usage", - "scenario_name": "Mobile Data Usage Check", - "success": true, - "tool_recall": 0.6666666666666666, - "tool_precision": 1.0, - "tool_f1": 0.8, - "keyword_coverage": 0.8, - "total_time": 9.09735631942749, - "tools_called": [ - "get_customer_detail", - "get_data_usage" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know their current data usage and ensure they are not close to their limit. Agent provided the exact usage (0 GB), the monthly cap (100 GB), and confirmed the user is well within their limit, fully resolving the intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant answered the user's query regarding their data usage and current limit. The user asked for their usage this month, and the assistant provided the data used (0 GB), the cap (100 GB), and confirmed the user is well within their limit. No extraneous or unrelated actions were taken, and no privacy, safety, or authorization rules were violated. There is no evidence of tool calls or external data being necessary or claimed erroneously. The result is fully usable and aligns with the user's intent.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response provides the customer's current data usage (0 GB), the monthly data cap (100 GB), and the billing cycle (January), which covers most of the key information required. It clearly compares usage to the limit and reassures the customer that they are well within their allowance. The agent also offers to monitor usage, provide daily details, and assist with plan upgrades, which are proactive options. However, the response does not specify the days remaining in the billing cycle or the percentage of data used, which are part of the ground truth solution for a perfect score. Therefore, it meets the \"Good\" criteria but falls short of \"Excellent.\"", - "llm_eval_time": 35.45230674743652 - }, - { - "scenario": "mobile_upgrade_premium", - "scenario_name": "Mobile Plan Upgrade", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 1.0, - "tool_f1": 0.5, - "keyword_coverage": 0.6, - "total_time": 8.596498727798462, - "tools_called": [ - "get_products" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted information on mobile plans with more data. The agent described the available plan, explained upgrade options for higher data tiers, and offered to assist further, fully addressing the user's intent with clear, relevant details.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant correctly responded to the user's request about mobile plans with more data, specifying the available plan and explaining that higher data tiers are available. It offered next steps including checking the user's current subscription and upgrading options, aligning with the user's goal and providing a clear path forward. There is no evidence of safety or privacy violations, nor of procedural mistakes regarding tool use or step sequencing. The response is relevant, complete within the available information, and does not include unrelated or distracting content.", - "llm_tool_score": 2.0, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is generic and does not personalize the information to the customer's actual situation. It fails to mention the customer's current plan (Essential, 5GB @ $29.99/month) and does not recommend the specific Premium plan ($59.99/month) with its associated benefits (unlimited data, international roaming, 5G priority, 50GB hotspot). The pricing and feature comparison are missing, and the response instead refers to a generic \"Contoso Mobile Plan\" and vague \"higher data tiers,\" which does not address the customer's specific upgrade path. While the agent offers to check the current subscription and help upgrade, the lack of accurate details and personalization means it only meets the \"Poor\" criteria in the rubric.", - "llm_eval_time": 36.427459478378296 - }, - { - "scenario": "mobile_hotspot_question", - "scenario_name": "Mobile Hotspot Inquiry", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.75, - "total_time": 11.687613010406494, - "tools_called": [ - "get_customer_detail", - "get_product_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know if their mobile plan includes hotspot for laptop use. Agent clearly explained that the current add-on does not include hotspot, noted the inactive status, and offered actionable next steps, fully resolving the intent with thoroughness and clarity.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant correctly identified the user's account and the specific subscription ('Contoso International Roaming'), explained that it does not include hotspot functionality, and noted the service is currently inactive. The assistant provided relevant next steps including options to view hotspot-enabled plans, check inactivity reasons, or switch/reactivate the plan. No unauthorized actions or privacy violations occurred, and no unverifiable or fabricated claims were made due to lack of corresponding tool calls. The output is complete, relevant, and does not violate any task, safety, or procedural requirement.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response thoroughly checks the customer's current subscription, identifies that the \"Contoso International Roaming\" add-on does not include hotspot/tethering, and clarifies that hotspot is a feature of base mobile plans. It also notes the account is inactive, which is a relevant detail affecting service availability. The agent offers clear next steps: adding or switching to a plan with hotspot, reactivating service, and offers to help find suitable plans or resolve the inactive status. This matches the ground truth solution by checking the plan, explaining hotspot status, and providing upgrade and usage options, fully addressing the customer's needs per the rubric.", - "llm_eval_time": 33.45222759246826 - }, - { - "scenario": "account_locked", - "scenario_name": "Account Locked After Failed Logins", - "success": true, - "tool_recall": 0.25, - "tool_precision": 1.0, - "tool_f1": 0.4, - "keyword_coverage": 0.8, - "total_time": 7.998707056045532, - "tools_called": [ - "unlock_account" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted help unlocking their locked account. The agent confirmed the account was locked for security reasons and stated it has now been unlocked, providing clear next steps and offering further assistance. The intent is fully resolved with thoroughness and accuracy.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user's intent was to get help with a locked account. The assistant claimed to have checked the account and unlocked it, implying real-world effect, but there are no supporting tool interactions or evidence that any account action was performed. This is a material failure, as claiming specific external actions are complete without corroborating evidence from the workflow or tools makes the output unreliable. No safety, privacy or rule violations are present otherwise.", - "llm_tool_score": 2.0, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response correctly identifies the root cause (\"locked for security reasons\"), confirms the account has been unlocked, and advises the customer to try logging in again. It also provides a security recommendation by suggesting a password reset if the customer still has trouble. However, it does not mention enabling two-factor authentication (2FA) or using a password manager, which are part of the ideal security recommendations in the ground truth solution. The response meets all the requirements for a score of 4 (\"Good\") per the rubric, as it verifies identity (implied by \"I've checked your account\"), unlocks the account, and provides at least one security recommendation, but falls short of the \"Excellent\" score due to missing additional security advice.", - "llm_eval_time": 37.88520669937134 - }, - { - "scenario": "account_security_check", - "scenario_name": "Security Audit Request", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 1.0, - "tool_f1": 0.5, - "keyword_coverage": 0.8, - "total_time": 9.805543899536133, - "tools_called": [ - "get_security_logs" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted confirmation that their account is secure following news of data breaches. Agent thoroughly checked security logs, reported no incidents, and provided safety tips and further assistance options, fully resolving the intent with clear, relevant information.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The assistant claimed to have reviewed security logs and found no security incidents associated with the user's account, implying access to real-time or external account data. However, there are no tool interactions or evidence provided to corroborate that any actual security check occurred. This is a material failure because the response suggests an action was taken that cannot be verified and may lead the user to rely on inaccurate information about account security. No sensitive information was exposed and presentation is appropriate, but the core verification claim is unsupported.", - "llm_tool_score": 5.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response demonstrates a good level of service by reviewing the customer's security logs and reporting that no suspicious activity or unauthorized access was found, directly addressing the customer's concern. It reassures the customer and provides several relevant security recommendations, such as using strong, unique passwords and being cautious of phishing attempts. However, it does not mention enabling 2FA, regular password updates, or explicitly advise against sharing credentials, which are part of the comprehensive recommendations in the ground truth solution. The offer to monitor for future issues and assist with password resets is helpful, but the response lacks the full breadth of security advice expected for an \"Excellent\" score. Thus, it meets the criteria for \"Good\" (score 4) but falls short of \"Excellent\" (score 5).", - "llm_eval_time": 42.52137589454651 - }, - { - "scenario": "account_update_contact", - "scenario_name": "Update Contact Information", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.6, - "total_time": 10.697373390197754, - "tools_called": [], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted their account contact information updated. The agent explained it cannot directly perform the update but offered to create a support ticket or guide the user through the process, requesting necessary details. This is a thorough and relevant next step toward resolving the intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user requested to update their account information with a new email and phone number. The assistant correctly acknowledged its limitations in directly updating contact details and offered appropriately relevant alternatives: creating a support ticket if the user provides the needed info, or guidance to update the information independently. The response is clear, presents all feasible options, and asks for needed clarification, without over-claiming actions or missing crucial steps. There are no issues in safety, privacy, or workflow adherence observable from the exchange. No material failures are present.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 4.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response acknowledges the customer's request to update contact information and provides guidance by offering to create a support ticket or explain the official process. However, it does not retrieve or show the current contact details, verify the customer's identity, or explain the verification process for new contact info, which are key actions outlined in the ground truth solution. The response is adequate in that it addresses the request and offers next steps, but it lacks the thoroughness and security considerations (such as how updates affect notifications, billing alerts, and password resets) required for a higher score. Therefore, it meets the criteria for a score of 3: Adequate.", - "llm_eval_time": 32.07609510421753 - }, - { - "scenario": "account_paperless_billing", - "scenario_name": "Paperless Billing Setup", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.8, - "total_time": 11.99247121810913, - "tools_called": [ - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to go paperless and stop receiving paper bills. The agent clearly explained how to enable paperless billing online, provided alternatives if the option isn't available, and offered further assistance, fully resolving the intent with thorough and relevant guidance.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant provided clear step-by-step instructions on how the user, customer 22, can go paperless and stop receiving paper bills by updating their preferences online. It also described alternate ways in case the online option was not available, such as contacting customer support or requesting through phone/chat. The assistant did not claim to complete any real-world actions without tool corroboration and no TOOL_CALLS were made. It respected privacy and did not expose sensitive data. There were no violations of workflow, authorization, or safety requirements. The output fully addresses the user's query and provides all necessary next steps.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response provides clear guidance on how the customer can enable paperless billing themselves, including step-by-step instructions for navigating the portal and alternative options if the online method is unavailable. However, the agent does not check the customer's current billing preferences, verify the email address on file, or directly enable paperless billing, which are key actions in the ground truth solution. The response does mention some benefits of paperless billing and offers to assist further (e.g., opening a support ticket), but it falls short of confirming changes or taking direct action. Therefore, it meets the \"Adequate\" criteria by providing guidance but does not reach the \"Good\" or \"Excellent\" levels as defined in the rubric.", - "llm_eval_time": 26.750492572784424 - }, - { - "scenario": "tv_channel_lineup", - "scenario_name": "TV Channel Lineup Question", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.5, - "total_time": 12.452617645263672, - "tools_called": [ - "get_customer_detail", - "get_product_detail" - ], - "llm_intent_score": 3.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know which TV channels are included in their streaming plan. The agent checked the account, explained no TV streaming plan is found, and asked for clarification to proceed. This is a relevant follow-up, but does not resolve the intent yet.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user asked for the channel lineup included with their TV streaming plan. The assistant checked the system and correctly clarified that the user's current subscription is a mobile + internet bundle, which does not include any TV streaming or channel information. Instead of fabricating an answer or ignoring ambiguity, it sought clarification about whether the user has a TV add-on or plan, offering reasonable next steps. There are no evidence-based failures relating to privacy, safety, or required workflows, and the system constraints were observed.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is poor according to the rubric. While it checks the customer's current subscription and correctly identifies that there is no TV streaming plan included, it does not provide any TV streaming plan details, features, or upgrade options as outlined in the ground truth solution. The agent only gives generic information about the customer's bundle and asks for clarification, failing to address the customer's actual need for TV channel or streaming plan information. This aligns with rubric level 2: \"Poor: Generic TV info without checking plan,\" since the agent does not move beyond stating the absence of TV service and does not proactively offer plan details or solutions.", - "llm_eval_time": 31.53426694869995 - }, - { - "scenario": "tv_add_sports", - "scenario_name": "Add Sports Package", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 1.0, - "tool_f1": 0.5, - "keyword_coverage": 0.5, - "total_time": 9.164242029190063, - "tools_called": [ - "get_products" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to add a sports package to watch football games. Agent clearly stated no dedicated sports package is available, suggested alternatives, and asked for more details to help further. The response is relevant and guides the user toward a solution, but does not fully resolve the intent yet.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant acknowledged the user's request to add a sports package for watching football games, looked for available options, and reported that no dedicated sports package was currently available. It offered relevant next steps by suggesting alternative packages, inviting clarification on the user's specific interests, and offered to search further based on the user's specific needs. There is no evidence of harm, privacy violations, unauthorized actions, or procedural errors. All requirements have been addressed appropriately and there are no material failures.", - "llm_tool_score": 5.0, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is poor because it provides generic information about sports packages and does not check the customer's current plan or offer a specific upgrade path. It incorrectly states that there is no dedicated sports package, missing the fact that sports channels are included in the Premium plan as per the ground truth. The agent does not mention pricing, the features of the Premium plan, or calculate the price difference. While the response is polite and offers to help further, it fails to address the customer's actual request to add sports channels and does not provide accurate or actionable solutions. This aligns with a score of 2 per the rubric.", - "llm_eval_time": 38.89677047729492 - }, - { - "scenario": "bundle_inquiry", - "scenario_name": "Bundle Package Inquiry", - "success": true, - "tool_recall": 0.6666666666666666, - "tool_precision": 0.6666666666666666, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 0.4, - "total_time": 16.00198459625244, - "tools_called": [ - "get_customer_detail", - "get_eligible_promotions", - "get_products" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know if bundling their internet and mobile services would save money. The agent clearly compared current and bundle costs, explained the value difference, and offered to help further, fully resolving the intent with thoroughness and accuracy.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant addressed the user's query about potential savings from bundling internet and mobile services. It provided a breakdown of current charges, explained the bundle option with its price, and compared costs clearly. The assistant also requested clarification about any separate mobile provider expenses, which is necessary given some ambiguity in the user's situation. There is no evidence of privacy, safety, authorization, or workflow violations, nor unsupported claims or missing required steps. All core requirements are met, and the response remains within scope and usable.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response provides bundle information and compares it to the customer's current setup, but it does not fully address the ground truth solution. While the agent checks the customer's current subscriptions and calculates the monthly cost, the bundle offered ($90/month) is not the \"Family Complete\" bundle described in the ground truth ($199.99/month with internet, TV, and 2 mobile lines). The agent does not mention the full bundle benefits (TV channels, multiple mobile lines, or the 20% discount), nor does it clearly show the value proposition or potential savings compared to individual services. The response is adequate in that it provides some relevant bundle information and invites further discussion, but it lacks the comprehensive comparison and explanation required for a higher score.", - "llm_eval_time": 31.322152614593506 - }, - { - "scenario": "promotion_eligibility", - "scenario_name": "Promotion Eligibility Check", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 1.0, - "total_time": 10.305032014846802, - "tools_called": [ - "get_eligible_promotions" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know if they, as customer 42, are eligible for any promotions or discounts. The agent checked and clearly stated there are none currently, while also offering relevant next steps. The intent is fully resolved with thoroughness.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user asked whether there were any promotions or discounts they were eligible for, identifying themselves as customer 42. The assistant replied that no current promotions or discounts were available for this customer, and offered relevant follow-up options. However, there is no evidence of tool usage or verification of this status, as TOOL_CALLS is empty. The assistant claimed access to the system and to have checked for eligibility, but no supporting data or confirmation is present, constituting a material procedural failure. Safety, privacy, and presentation rules appear respected, but claiming real-world external action without corroborating evidence is a material issue.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is poor because it provides a generic statement that the customer is not eligible for any promotions, without specifying which promotions exist or checking eligibility against the criteria outlined in the ground truth (loyalty level, number of services, new customer status, etc.). While the agent offers to check active promotions and review subscriptions, they do not actually list available promotions or explain how the customer could qualify for them. This falls short of identifying applicable promotions or explaining how to take advantage of offers, as required for higher scores. The response is more focused on process than on providing concrete, helpful information about promotions.", - "llm_eval_time": 28.650881052017212 - }, - { - "scenario": "loyalty_benefits", - "scenario_name": "Loyalty Program Benefits", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.3333333333333333, - "total_time": 9.727946758270264, - "tools_called": [ - "get_eligible_promotions" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know what loyalty benefits they receive as a long-term customer. The agent clearly checked the account, explained no current benefits, and offered relevant next steps, fully resolving the intent with thoroughness and transparency.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user's objective was to find out what loyalty benefits they receive as a long-term customer (Customer 48). The assistant acknowledged the user's loyalty, checked for any active loyalty or promotional offers for their account, and communicated that none are currently available. The response explained why no benefits are available and offered relevant next steps, such as checking current promotions or having a specialist review the account. There is no evidence of missing required elements, unsafe content, privacy violations, or procedural missteps (e.g., unsubstantiated claims requiring tool verification). The assistant did not claim any actions or data unsupported by tool interactions, nor was there a presentation constraint. The outcome is accurate and usable for the user's inquiry.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is poor according to the rubric. While it acknowledges the customer's loyalty and checks for current promotions, it does not identify or mention the customer's loyalty tier, nor does it explain the benefits associated with any tier. The response provides generic information about eligibility and offers to check for promotions or review plans, but it fails to address the specific loyalty program structure, benefits, or upgrade paths as outlined in the ground truth solution. There is no mention of current Gold/Platinum promotions or how the customer could reach a higher tier. Therefore, the response only provides generic loyalty info without checking or explaining the customer's level, which matches the rubric's definition of a score of 2.", - "llm_eval_time": 29.205983638763428 - }, - { - "scenario": "support_ticket_status", - "scenario_name": "Support Ticket Status Check", - "success": true, - "tool_recall": 0.5, - "tool_precision": 1.0, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 0.8, - "total_time": 8.832990884780884, - "tools_called": [ - "get_support_tickets" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know the status of their support ticket. The agent provided detailed information about the latest ticket, including its status, resolution date, and handler, fully resolving the intent with accuracy and completeness.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant provided a direct update on the user's support ticket status, clearly referencing the details relevant to Customer 6, and indicated it was closed. There is no evidence of safety, privacy, or workflow violations, and all information is appropriate and supports the user's explicit request. The tool call evidence matches the assistant's statements, and the output is both complete and usable with a helpful follow-up offered. There are no material failures in fulfilling the user's objective or respecting default rules given the content and context.", - "llm_tool_score": 5.0, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response accurately finds and reports the ticket status, providing specific details such as the ticket ID, category, subject, priority, opened/closed dates, and the staff member who handled it. It clearly states that the ticket is closed and offers a next step by inviting the customer to reopen or create a new ticket if the issue persists. However, it does not explain the resolution process or what was done to resolve the ticket, nor does it provide an expected resolution timeline or offer escalation options for unresolved issues. According to the rubric, this meets the criteria for a \"Good\" response (score 4), as it provides ticket status and explanation, but lacks the full detail and proactive escalation found in an \"Excellent\" response (score 5).", - "llm_eval_time": 38.70934796333313 - }, - { - "scenario": "support_new_ticket", - "scenario_name": "Create New Support Ticket", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.4, - "total_time": 10.851263523101807, - "tools_called": [ - "get_customer_detail", - "create_support_ticket" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted someone to look into their cable box issue. The agent confirmed a support ticket was created, provided details, and explained next steps, fully resolving the intent with clear communication and relevant follow-up advice.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user's request was for someone to look into the issue of their cable box randomly rebooting. The assistant responded by confirming that a support ticket was created, summarizing the issue accurately and providing a next step (technical team will review and may contact the user). The status is reported as open, and the asynchronous nature of resolution is made clear. There is no evidence of unsafe behavior, privacy violation, or failure to follow required workflows; the assistant does not claim any unverified tool actions. All relevant information is included, and the output is fully usable for the user\u2019s intent.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response documents the issue (cable box rebooting randomly), creates a support ticket with relevant details (issue, category, priority, ticket ID, status), and informs the customer about next steps. It also invites the customer to provide additional information if patterns are noticed, which is helpful for troubleshooting. However, the response does not mention any basic troubleshooting steps (such as unplugging the box or checking connections) nor does it offer a technician visit if needed, both of which are specified in the ground truth solution for a top score. Therefore, while the response is good and covers most requirements, it falls short of \"Excellent\" due to missing troubleshooting and technician offer.", - "llm_eval_time": 33.28236222267151 - }, - { - "scenario": "multi_billing_dispute", - "scenario_name": "[Multi-Turn] Billing Dispute Resolution", - "success": true, - "tool_recall": 0.6666666666666666, - "tool_precision": 1.0, - "tool_f1": 0.8, - "keyword_coverage": 0.6, - "total_time": 18.855677127838135, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 3.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted an explanation for a $50 charge on their bill. The agent asked for customer ID to access the account, which is a necessary step before providing the requested information. The response is relevant and moves towards resolution, but does not yet answer the question.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant responded to the user's request about an unknown $50 charge by asking for additional information (customer ID or permission to look up the account), which is a reasonable clarifying step needed to proceed. The assistant did not provide unrelated information or take any unauthorized actions. No material failures related to goal adherence, privacy, or required workflows are present.", - "llm_tool_score": 2.0, - "llm_coherence": 3.0, - "llm_fluency": 4.0, - "llm_relevance": 3.0, - "llm_solution_score": 1.0, - "llm_solution_reason": "The agent's response fails to address the billing dispute scenario as outlined in the ground truth solution. It does not investigate the $50 charge, explain its origin, or handle any credit requests. Instead, it immediately asks for customer identification and jumps to checking promotions, skipping the critical steps of charge investigation and resolution. There is no evidence of understanding the customer's actual issue or providing accurate information about the disputed charge. The response loses conversation context and does not meet any of the key criteria in the rubric, warranting a score of 1.", - "llm_eval_time": 28.704562187194824 - }, - { - "scenario": "multi_internet_troubleshoot", - "scenario_name": "[Multi-Turn] Internet Troubleshooting Flow", - "success": true, - "tool_recall": 0.25, - "tool_precision": 0.5, - "tool_f1": 0.3333333333333333, - "keyword_coverage": 0.8333333333333334, - "total_time": 27.918824195861816, - "tools_called": [ - "get_customer_detail", - "create_support_ticket" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted help with frequent internet drops and likely expects troubleshooting or a technician visit. Agent clearly explains the next steps, requests needed info, and sets expectations about scheduling, effectively moving towards resolving the issue.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant responded appropriately by asking for necessary clarifying details before proceeding with the user's request for technical support and a technician visit. It did not claim to perform any real-world action without corroborating evidence and made no unsafe or unauthorized actions. No mandated formats or strict constraints were violated, and no privacy or safety issues are present. The workflow aligns with the instructions, and the assistant clearly disclosed its limitations and next steps.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response skips the initial troubleshooting and diagnostic steps outlined in the ground truth solution, moving directly to scheduling a technician visit. While it does request relevant information to create a support ticket and mentions that the scheduling team will follow up, it does not build on previous troubleshooting attempts or acknowledge any prior steps the customer may have taken. The response is adequate in that it offers a next step and seeks to address the customer's need for a technician, but it lacks continuity and progression through the troubleshooting flow, as required for a higher score. It also does not express empathy or confirm the issue has been properly diagnosed before escalation, which are important elements in the rubric for a score of 4 or 5.", - "llm_eval_time": 23.407470703125 - }, - { - "scenario": "multi_service_cancellation", - "scenario_name": "[Multi-Turn] Service Cancellation Retention", - "success": false, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.6666666666666666, - "total_time": 17.540900230407715, - "tools_called": [], - "llm_intent_score": 2.0, - "llm_intent_result": "fail", - "llm_intent_reason": "User wanted to cancel their internet service due to high cost. The agent instead offered a discount and requested more details, failing to address the cancellation request and redirecting the conversation away from the user's intent.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user requested to cancel their internet service due to the high cost. The assistant instead asked for details to apply a 20% discount and did not acknowledge the cancellation request. This is a material failure as it did not address the user's stated intent to cancel, nor did it provide next steps for cancellation or seek clarification on ambiguity. No issues of safety, privacy, or procedural errors were evident, but the primary goal of enabling cancellation was not adhered to.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 1.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response is adequate in that it attempts retention by referencing a 20% discount and asks for details to calculate the new rate, which aligns with part of the ground truth solution. However, it misses key elements: there is no empathy or acknowledgment of the customer's cancellation reason, no discussion of competitor pricing or value-adds, and no effort to understand the customer's specific needs. The response is transactional and lacks personalization, failing to fully address the customer's concerns or secure retention as outlined in the rubric for higher scores.", - "llm_eval_time": 26.475765228271484 - }, - { - "scenario": "multi_new_customer_setup", - "scenario_name": "[Multi-Turn] New Service Setup Assistance", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.7142857142857143, - "total_time": 31.669695377349854, - "tools_called": [ - "get_products", - "get_promotions" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know their internet setup options after moving to a new apartment. The agent requested necessary information to proceed, clarified limitations, and outlined next steps, effectively moving toward resolving the intent but not fully completing it yet.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant responded by requesting necessary information (customer ID, account status, and clarification about promotions) before proceeding, since it needs these details to accurately provide internet options or make changes. This is a valid step for helping a user set up an internet plan, especially since the assistant cannot create a new subscription and made this transparent. There are no privacy breaches, unsafe actions, or procedural missteps in the response. The output is on-topic, appropriately scoped, and safely seeks required inputs to progress the task.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 3.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is poor because it fails to guide the customer through the new customer setup process as outlined in the ground truth solution. Instead of presenting available internet plans, explaining speed tiers, and asking about usage needs (Turn 1), the agent immediately requests account information and states an inability to create a new subscription, which is a critical failure for a new customer scenario. There is no personalized recommendation, no mention of promotions, and no attempt to complete the setup or welcome the customer. The experience is disjointed and misses key steps, particularly the sales flow and application of promotions, which are essential for an excellent or good score according to the rubric.", - "llm_eval_time": 24.824542999267578 - }, - { - "scenario": "multi_complex_account_issue", - "scenario_name": "[Multi-Turn] Complex Account Resolution", - "success": false, - "tool_recall": 0.4, - "tool_precision": 1.0, - "tool_f1": 0.5714285714285715, - "keyword_coverage": 0.875, - "total_time": 29.227781295776367, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 2.0, - "llm_intent_result": "fail", - "llm_intent_reason": "User wanted help with being charged for a cancelled service. The agent did not address the billing issue or offer to investigate the charge, instead providing a generic summary of account changes and asking for more details, leaving the intent largely unresolved.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user reported being charged for a service they cancelled last month and, as customer 11, was expecting resolution or investigation. The assistant did not acknowledge the specific billing issue nor offer to investigate, instead describing what actions had not yet occurred and asking for clarification on past requests and customer ID. While requesting more specific information for verification may be justified, the assistant did not directly address or attempt to resolve the user's central concern about the charge. This falls short of meeting the user's objective and is a material failure. No privacy or procedural breaches were detected.", - "llm_tool_score": 2.0, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 3.0, - "llm_solution_score": 1.0, - "llm_solution_reason": "The agent's response fails to address any of the customer's three issues (billing, internet, TV downgrade) as outlined in the ground truth solution. It does not identify the root causes, provide accurate information, or offer any solutions or next steps. Instead, it states that no changes have been made and asks the customer to clarify what they want changed, which ignores the multi-turn context and the specific actions required. The response does not maintain context or provide a summary, and it does not meet any of the criteria for higher scores in the rubric.", - "llm_eval_time": 32.86390948295593 - } - ], - "reflection": [ - { - "scenario": "billing_high_invoice", - "scenario_name": "Invoice Higher Than Usual", - "success": true, - "tool_recall": 0.4, - "tool_precision": 1.0, - "tool_f1": 0.5714285714285715, - "keyword_coverage": 0.6666666666666666, - "total_time": 31.48013162612915, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to understand why their last invoice was much higher than usual. The agent explained the overage charges, payment status, and possible reasons for increased usage, offering further breakdowns and solutions. The response is thorough and directly addresses the user's intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant addressed the user's concern by providing a detailed explanation for the higher-than-usual invoice, citing information about the overage charges and payment details. It offered to break down the charges further and gave options for resolving or understanding usage. There is no evidence of privacy or safety violations, nor did the assistant attempt any unauthorized real-world actions. No external data was claimed beyond the user's query and no tool calls were present that contradict the explanation. The assistant's output is complete, relevant, and there are no material failures evident.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response identifies overage charges as the cause of the higher invoice and mentions the 10GB data cap, which aligns with the ground truth. However, it does not specify the actual data usage (22GB), the exact amount of overage (12GB), or the per-GB overage rate ($7.50/GB), which are key details for a clear explanation. The response offers some solution options, such as upgrading the plan and reviewing usage, but does not mention a one-time courtesy adjustment or setting up data alerts. The explanation is adequate but lacks the specific numbers and full range of recommended solutions, resulting in a score of 3 per the rubric.", - "llm_eval_time": 30.805689334869385 - }, - { - "scenario": "billing_payment_history", - "scenario_name": "Payment History Inquiry", - "success": true, - "tool_recall": 0.5, - "tool_precision": 0.5, - "tool_f1": 0.5, - "keyword_coverage": 0.4, - "total_time": 16.960052967071533, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to see their recent payments and confirm they all went through. Agent provided a detailed, accurate summary of successful and problematic payments, clearly indicating which were processed and which need attention, fully resolving the intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user's request was to see recent payments and confirm if they all went through. The assistant produced a detailed summary that directly lists all recent payments, distinguishes those that were successful, and notes those with outstanding balances or failed payments. This response is clear, complete, and actionable. No evidence of privacy violations, procedural errors, or ignoring strict instructions is present. Although no tool outputs are shown to confirm external verification, there is no indication that tool use was required or that unverifiable claims were made; the response is within reasonable expectations for an account summary. No material failures are identified.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response fully meets the ground truth solution and scoring rubric for an excellent answer. It provides a detailed payment history, listing each invoice with dates, amounts, and payment methods used. The response clearly distinguishes between successful payments and those that are pending or failed, specifying the remaining balances for each outstanding invoice. It confirms that all listed successful payments were processed and that there is no remaining balance on those invoices. The agent also offers helpful next steps, such as assistance with paying outstanding balances, retrying failed payments, and setting up autopay, which aligns with the \"helpful actions\" in the ground truth. Overall, the response is thorough, accurate, and customer-focused, addressing all aspects of the customer's needs.", - "llm_eval_time": 35.96636772155762 - }, - { - "scenario": "billing_autopay_setup", - "scenario_name": "Autopay Setup Request", - "success": true, - "tool_recall": 0.25, - "tool_precision": 0.3333333333333333, - "tool_f1": 0.28571428571428575, - "keyword_coverage": 0.6, - "total_time": 45.64754843711853, - "tools_called": [ - "get_customer_detail", - "update_subscription", - "update_subscription", - "update_subscription", - "update_subscription", - "update_subscription", - "create_support_ticket" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted help setting up autopay for their bill. The agent explained the current account status, outlined the steps to enable autopay, and requested confirmation to proceed, effectively moving towards resolution with clear next actions. Minor delay due to manual review, but intent is well addressed.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant addressed the user's goal by explaining the current account status and offering to initiate a billing request to set up autopay, which aligns with the user's intent and request. The assistant sought confirmation before taking any action, ensuring proper user consent and procedural correctness, and did not claim any critical actions were completed without corroborating evidence from external sources. No sensitive information was exposed and no unauthorized or high-risk actions were taken. There were no presentation or workflow violations. Overall, the assistant followed a safe, appropriate, and complete workflow with justified next steps.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response is thorough and customer-focused, correctly identifying the autopay status (not enabled) and checking the account details, including the subscription's inactive status. It explains how autopay works and outlines the next steps, including confirming the payment method and offering to open a billing request. However, the response does not mention the $5 monthly discount associated with autopay, which is a key benefit per the ground truth solution. While the agent guides the customer through the setup process and addresses potential issues with the inactive subscription, the omission of the discount prevents it from achieving a perfect score. Overall, the response meets most requirements but lacks one critical piece of information.", - "llm_eval_time": 27.532016277313232 - }, - { - "scenario": "billing_overdue_invoice", - "scenario_name": "Overdue Invoice Question", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 1.0, - "total_time": 17.404196977615356, - "tools_called": [ - "get_billing_summary", - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know the consequences of not paying an overdue invoice. The agent clearly explained possible outcomes, including late fees, service suspension, and escalation, addressing the intent thoroughly and accurately with relevant details and options for next steps.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user's task was to understand the consequences of not paying an overdue invoice. The assistant provided a clear, relevant, and detailed response outlining possible outcomes (late fees, suspension, loss of discounts, escalation), tailored to the user's account and invoices. There is no violation of safety, privacy, or presentation rules; no sensitive information is exposed beyond what is appropriate for the user identified as customer 15. The assistant did not claim any external actions or results not supported by tool data, nor did it misrepresent or fabricate information. All pertinent information is delivered, and no required workflow or tool use is missing. There are no material failures.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response fully meets the ground truth solution and scoring rubric for an excellent answer. It lists the specific overdue invoices (160, 165, and 167) with the total overdue amount ($235.93), directly addressing the customer's concern. The response clearly explains the consequences of non-payment, including late fees, service suspension, loss of discounts, and escalation to collections, which aligns with the required explanation of late fee policy and potential service suspension after 30+ days overdue. It also offers multiple payment options and solutions, such as immediate payment, payment arrangements, and support for billing disputes, fulfilling the recommended actions. The response is specific, accurate, and customer-focused, addressing all aspects of the scenario as outlined in the ground truth.", - "llm_eval_time": 24.46917414665222 - }, - { - "scenario": "billing_refund_request", - "scenario_name": "Refund Request for Service Issue", - "success": true, - "tool_recall": 0.5, - "tool_precision": 1.0, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 0.8, - "total_time": 30.160746097564697, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User requested a refund or credit for 3 days without internet. The agent explained eligibility, estimated the credit, and outlined next steps, but did not initiate the adjustment yet\u2014pending user confirmation. The response is thorough and actionable, but not fully complete.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant correctly understood the user's request for a refund or credit due to a service outage, provided a personalized response, and explained the requirements for a prorated credit. It outlined next steps and requested necessary information to proceed, which is appropriate given the partial evidence from the account and incident status. No unauthorized actions were taken, and no sensitive data was exposed. There were no claims of external actions that lack corroboration in the tool calls. The response aligns with safety, privacy, and procedural expectations, and follows a justified workflow by requesting further clarification before adjustment.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response acknowledges the issue, checks the account for a relevant service incident, and explains the process for applying a prorated credit, including an accurate estimate of the amount ($9 for 3 days). The agent also outlines the necessary steps to confirm the outage and offers to open a billing adjustment ticket. However, the response does not fully verify the outage or immediately apply the credit; instead, it requests further confirmation from the customer before proceeding. While thorough and customer-focused, it falls short of the \"Excellent\" rubric level, which requires verification and confirmation of the credit being applied. The response meets the \"Good\" criteria by acknowledging the issue and offering an appropriate credit, but is missing the final confirmation and application steps.", - "llm_eval_time": 34.852864265441895 - }, - { - "scenario": "internet_slow", - "scenario_name": "Internet Slower Than Before", - "success": true, - "tool_recall": 0.5, - "tool_precision": 1.0, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 0.6, - "total_time": 28.77719473838806, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User reported slow internet despite paying for 1Gbps. Agent checked account, acknowledged the issue, suggested troubleshooting steps, and explained next actions pending user confirmation. The response is thorough and moves towards resolution, but does not yet resolve the issue fully.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant properly addressed the internet speed complaint by checking the account status, referencing a relevant previous support incident, and outlining steps the user can take for diagnostics. It clearly disclosed that no external changes or escalations were made yet, correctly implying that no tool actions have occurred, which matches the absence of tool outputs. The assistant asked a clarifying question to pinpoint whether the issue persists and which devices are affected, which is appropriate before escalating or taking further action. No sensitive data was exposed, and no unsafe or rule-violating actions occurred. The workflow is appropriate and sequenced correctly, with the assistant waiting for user confirmation before proceeding. There are no material failures in any dimension.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response correctly identifies the existing service incident, referencing the April 17 report and its current \"investigating\" status, which aligns with the ground truth. The agent provides clear troubleshooting steps (restart modem/router, run a wired speed test, check Wi-Fi), and offers to escalate or open a new support ticket depending on the customer's confirmation of the issue. However, the response does not explicitly mention the possibility of a service credit once the issue is resolved, which is a key part of the ideal solution for a score of 5. Overall, the response is thorough, empathetic, and proactive, but misses the final element required for an \"Excellent\" rating.", - "llm_eval_time": 37.12612819671631 - }, - { - "scenario": "internet_upgrade_inquiry", - "scenario_name": "Internet Speed Upgrade Options", - "success": true, - "tool_recall": 0.5, - "tool_precision": 0.6666666666666666, - "tool_f1": 0.5714285714285715, - "keyword_coverage": 0.5, - "total_time": 30.55335831642151, - "tools_called": [ - "get_customer_detail", - "get_products", - "get_eligible_promotions" - ], - "llm_intent_score": 3.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted upgrade options for faster internet to support video calls. The agent reviewed the account, explained the offline status, and outlined upgrade possibilities, but did not provide specific upgrade options or pricing, making the resolution informative but incomplete.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant responded appropriately by reviewing the user's account information and addressing the issue related to slow internet. It identified that the service was offline and suggested resolving this before discussing upgrade options, which is a reasonable procedural and goal-oriented approach. No external data or actions were claimed without verification, and privacy was maintained. No strict presentation requirements were given. The assistant offered clear next steps and did not fabricate any details or bypass necessary sequences. Thus, there are no material failures present.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response is adequate but falls short of the ideal solution. It does a good job of personalizing the reply by referencing the customer's current plan details (speed tier, data cap, loyalty level, and service status), and it correctly identifies a potential root cause (offline status) that may be affecting performance. However, it does not present specific upgrade options with pricing, nor does it recommend a plan based on the customer's stated need (video calls). The response mentions that upgrade options and pricing are subject to availability and need to be confirmed, but does not list the actual tiers or prices as required by the ground truth. Promotions are referenced in general terms (\"Gold loyalty offers\"), but no concrete offers are provided. Overall, the agent lists upgrade possibilities and next steps but misses key details and recommendations, making this an \"Adequate\" (score 3) response per the rubric.", - "llm_eval_time": 26.95149254798889 - }, - { - "scenario": "internet_router_reset", - "scenario_name": "Router Reset Help", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.8333333333333334, - "total_time": 13.841727018356323, - "tools_called": [ - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted clear instructions on how to reset their router. The agent provided detailed, step-by-step guidance, explained important consequences, and offered further help if needed. The response is thorough, accurate, and fully resolves the user's intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant provided clear, step-by-step instructions on how to reset a home router, addressing the user's immediate request without introducing extraneous or irrelevant content. The assistant also noted the effects of a factory reset and offered further help, covering possible next steps if the problem persisted. There are no signs of unsafe, unauthorized, or privacy-violating actions, and no deceptive claims about external actions or data (as no TOOL_CALLS were present). The information remains within the bounds of the user's request and does not violate any apparent workflow or presentation requirements. All evaluation areas pass.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response provides complete, step-by-step instructions for resetting the router, including locating the reset button, using a paperclip, holding the button for the correct amount of time, and waiting for the router to reboot. It clearly warns that a factory reset will erase custom settings and explains what those settings might be, which aligns with the rubric's requirement to mention the factory settings warning. The response also offers additional help, such as checking for service outages, opening a support ticket, or assisting with Wi-Fi reconfiguration, and suggests unplugging the modem and router if issues persist. These elements fulfill all criteria for an \"Excellent\" score according to the rubric.", - "llm_eval_time": 26.97669816017151 - }, - { - "scenario": "internet_outage_report", - "scenario_name": "Internet Outage Report", - "success": true, - "tool_recall": 0.5, - "tool_precision": 1.0, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 1.0, - "total_time": 16.266195058822632, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know if there was an internet outage in their area. The agent thoroughly checked the account and area status, confirmed no outage, and provided clear next steps for troubleshooting and escalation, fully resolving the user's intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user's objective was to determine if there is an internet outage in their area, given that their internet is completely down. The assistant checked the account status, service status, and outage reports, and communicated that there were no outages currently reported for the user's area. It provided relevant troubleshooting steps and offered to open a priority support ticket if the issue persisted. There are no inappropriate disclosures, harmful actions, or workflow violations evident, and the assistant did not claim any actions or data that required external tool verification. The response is complete, on topic, and presents appropriate next steps, fully meeting the user's intent.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response fully aligns with the ground truth solution and meets all criteria for an excellent score. The agent checks the customer's subscription status, confirms there are no reported outages or incidents in the area, and verifies the account is active. Since no outage is detected, the agent provides clear troubleshooting steps and offers to open a priority support ticket for further investigation, including a technician visit if needed. The response is empathetic, accurate, and proactive, addressing the customer's needs and offering immediate next steps. While the agent does not explicitly mention service credit for extended outages, this is only relevant if an outage is confirmed, which is not the case here. Overall, the response is thorough and customer-focused, matching the ideal solution.", - "llm_eval_time": 38.44671297073364 - }, - { - "scenario": "internet_static_ip", - "scenario_name": "Static IP Request", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.5, - "total_time": 20.508050441741943, - "tools_called": [ - "search_knowledge_base" - ], - "llm_intent_score": 3.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know if a static IP address is available for their home server. The agent explained the general policy and offered to check the user's specific plan, moving the resolution forward but not fully answering the question yet.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant's response accurately addresses the user's request by explaining that static IP availability depends on the user's current plan and offers to check the user's details (Customer 40) before proceeding. There is no irrelevant information, no fabrication of results, or unverified claims, and no external actions performed without evidence from tool interactions. Safety, privacy, and procedural compliance are maintained, and the assistant does not overstep any boundaries, exposure, or presentation constraints. The output is usable, complete as a justified partial result (with offered next steps), and aligns with the user's intent.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response acknowledges the customer's request for a static IP and offers to check the customer's current plan, which aligns with the first recommended action in the ground truth. However, it lacks specific details about which Contoso plans include static IPs, their pricing, and the fact that the Basic plan does not offer this feature. The response generically mentions that some plans may support static IPs as an add-on and that a plan change or upgrade may be required, but does not specify the actual plans or costs. While the agent does mention the benefits of a static IP and offers to review the customer's subscription, the absence of concrete plan information and clear recommendations means it meets the \"Adequate\" criteria in the rubric.", - "llm_eval_time": 28.51239562034607 - }, - { - "scenario": "roaming_travel", - "scenario_name": "Travelling Abroad - Needs Roaming", - "success": true, - "tool_recall": 0.25, - "tool_precision": 1.0, - "tool_f1": 0.4, - "keyword_coverage": 0.5, - "total_time": 12.257369756698608, - "tools_called": [ - "get_customer_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted information about international roaming for their upcoming trip to Spain. The agent provided a thorough overview of the current roaming status, relevant details, costs, and next steps, fully addressing the user's intent with clear, actionable information.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant provided appropriate and relevant information about international roaming for customer 253, including the current roaming status, guidance on enabling the feature before travel, and offered actionable next steps. The information was presented clearly and did not violate any safety, privacy, or authorization rules, nor was there any unsupported claim of tool-based action or real-world effects. There were no required workflows, presentation formats, or external data verifications explicitly mandated by the prompt that were missed. Therefore, there is no material failure in any dimension.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response correctly identifies that international roaming is currently disabled on the customer's account and acknowledges the urgency of enabling it before travel in 2 days. It offers to enable roaming and mentions the availability of international roaming add-ons or promotions, which aligns with recommending a package. However, the response does not explicitly mention the typical 3-day activation timeline or the potential need for expedited activation, which is a key point in the ground truth solution. It also does not specify that Spain is covered under European roaming options or detail the types of add-ons (voice, text, data) available. Overall, the response is accurate and helpful but misses some critical details required for a perfect score.", - "llm_eval_time": 39.21615028381348 - }, - { - "scenario": "mobile_data_usage", - "scenario_name": "Mobile Data Usage Check", - "success": true, - "tool_recall": 0.6666666666666666, - "tool_precision": 1.0, - "tool_f1": 0.8, - "keyword_coverage": 1.0, - "total_time": 29.104389429092407, - "tools_called": [ - "get_customer_detail", - "get_data_usage" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know their current data usage to avoid exceeding their limit. The agent provided a clear, accurate summary of usage, remaining data, and offered relevant options for monitoring. The response fully resolves the intent with thoroughness and helpful context.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user requested their current data usage for the month, expressing concern about remaining within their limit. The assistant provided specific data usage details, including amount used, cap, and percentage, and contextualized the date range (January 1 \u2013 January 14), which is reasonable given ambiguity about the user's exact billing cycle. While no tool data is present, there is no explicit requirement for tool use or external verification, so sharing this information does not imply unsupported external claims. The assistant avoided unsafe, unauthorized, or privacy-violating actions and did not violate presentation or procedural requirements. The summary provided is complete and actionable for the user's inquiry.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response fully meets the criteria for an excellent answer. It provides the customer's current data usage (0 MB), the data cap (100 GB), the percentage used (0%), and clarifies the time frame (January 1 \u2013 January 14), which helps the customer understand where they are in the billing cycle. The agent proactively mentions the possibility of overage charges or reduced speeds, suggests enabling usage alerts, and offers to assist with plan upgrades or daily usage details. This covers all key information and proactive advice outlined in the ground truth solution, directly addressing the customer's needs and offering clear next steps.", - "llm_eval_time": 35.96887159347534 - }, - { - "scenario": "mobile_upgrade_premium", - "scenario_name": "Mobile Plan Upgrade", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.6, - "total_time": 15.033612251281738, - "tools_called": [ - "get_products", - "get_product_detail" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted information on mobile plans with more data. The agent described a relevant plan with higher data tiers and offered next steps, but did not specify actual data amounts or alternative plans, leaving some details vague. Overall, the intent is mostly resolved.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user asked for mobile plans with more data because they keep running out of data. The assistant responded with a relevant plan (Contoso Mobile Plan) emphasizing higher data tiers available, the starting monthly fee, and noted that it's suitable for users who run out of data. It offered relevant next steps (checking the user's current plan, recommending best tier, checking promotions, and upgrading), which are all in scope and actionable. No safety, privacy, or presentation contract was violated, and no external actions or unverifiable claims were made (TOOL_CALLS is empty, but the reply is general and does not claim real-world changes or specific user/account data). There are no omitted steps or required workflow violations. The output is usable and complete for the user's request.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response is adequate but lacks key details required for a higher score. While it acknowledges the customer's interest in upgrading and offers to check the current plan and recommend a higher data tier, it does not specify the customer's current plan (Essential, 5GB @ $29.99/month) or clearly recommend the Premium plan with its price ($59.99/month) and benefits (unlimited data, international roaming, 5G priority, 50GB hotspot). The response is somewhat generic, mentioning \"higher data tiers\" and a starting price of $50/month, but does not provide a direct comparison or highlight the specific advantages of upgrading. It does offer to process the upgrade and check for promotions, which is helpful, but the lack of personalization and missing details about the recommended plan prevent it from scoring higher.", - "llm_eval_time": 26.51233434677124 - }, - { - "scenario": "mobile_hotspot_question", - "scenario_name": "Mobile Hotspot Inquiry", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 1.0, - "total_time": 12.74303936958313, - "tools_called": [ - "get_customer_detail", - "get_product_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know if their mobile plan includes hotspot for laptop use. The agent clearly checked the account, explained that hotspot is not included, and provided next steps for enabling it, fully resolving the intent with thoroughness and accuracy.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user's request was to know whether their mobile plan includes hotspot functionality for use with a laptop. The assistant directly answers this, stating that the mobile plan does not include hotspot, and provides details about the subscription showing it is an international roaming add-on only. There is no unsupported claim of tool use, real-world action, or invented data, and no privacy, safety, or workflow errors are present. The additional suggestions for next steps are relevant and do not distract from the deliverable. No material failures are present.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response fully meets the criteria for an excellent answer. It checks the customer's current plan and clearly explains that the subscription is only for an international roaming add-on, which does not include hotspot functionality or a full mobile data plan. The agent accurately identifies the root cause (no active mobile data plan, no hotspot feature), provides relevant details (subscription status, data cap, product type), and offers appropriate next steps by suggesting plans that include hotspot and offering to assist with activation or upgrades. The response is specific, informative, and directly addresses the customer's needs regarding hotspot usage, aligning perfectly with the ground truth solution and scoring rubric for a 5.", - "llm_eval_time": 20.887587785720825 - }, - { - "scenario": "account_locked", - "scenario_name": "Account Locked After Failed Logins", - "success": true, - "tool_recall": 0.5, - "tool_precision": 1.0, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 0.8, - "total_time": 11.602241516113281, - "tools_called": [ - "get_security_logs", - "unlock_account" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted help unlocking their locked account. The agent confirmed the reason for the lock, unlocked the account, and provided clear next steps and preventative advice, fully resolving the user's intent with thoroughness and accuracy.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The assistant responded to the user's report of a locked account by claiming to have unlocked it and provided guidance on next steps. However, there is no evidence in the tool interactions indicating that the assistant actually performed the unlocking action; the tool interactions are empty. Because the assistant's claim of unlocking the account implies a real-world effect that is not corroborated by tool use, this is a material failure. There are no issues related to safety/authorization, but the lack of procedure adherence is critical here.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response fully meets the criteria for an excellent solution. It correctly identifies the root cause of the lockout (\"your account was locked due to multiple failed login attempts\"), confirms that the account has been unlocked, and provides clear next steps. The agent recommends using the \"Forgot Password\" option if the customer is unsure of their password, suggests enabling multi-factor authentication for added security, and advises on best practices to prevent future lockouts. All required actions and recommended follow-ups from the ground truth are addressed, including identity verification (via customer ID), unlocking the account, confirmation of success, and multiple security recommendations.", - "llm_eval_time": 36.84952211380005 - }, - { - "scenario": "account_security_check", - "scenario_name": "Security Audit Request", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 1.0, - "tool_f1": 0.5, - "keyword_coverage": 1.0, - "total_time": 11.841587543487549, - "tools_called": [ - "get_security_logs" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted confirmation that their account is secure following news of data breaches. The agent thoroughly checked for incidents, reported no issues, and provided relevant security advice, fully resolving the intent with clear, accurate, and proactive support.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user requested a security check for their account (Customer 12). The assistant claims to have reviewed the account and found no incidents, but there are no tool interactions provided to verify any check was actually performed. The claim of reviewing security activity and stating results implies real-world verification, which cannot be substantiated without evidence from tool calls. This is a material issue, as the assistant's response cannot be trusted to fulfill the user's objective.", - "llm_tool_score": 5.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 5.0, - "llm_solution_reason": "The agent's response thoroughly reviews the customer's security activity, explicitly stating that no suspicious events, unauthorized login attempts, or breaches were found, which aligns with the ground truth's requirement to review logs and verify no unauthorized access. The agent then provides comprehensive security recommendations, including using strong passwords, enabling 2FA, being cautious of phishing, and logging out of shared devices. Additionally, the agent offers further assistance, such as reviewing login activity in more detail and helping update security settings, which goes beyond basic advice. The response is reassuring and explains the security measures in place, directly addressing the customer's needs and matching all key actions and recommendations outlined in the ground truth solution.", - "llm_eval_time": 32.73884558677673 - }, - { - "scenario": "account_update_contact", - "scenario_name": "Update Contact Information", - "success": true, - "tool_recall": 1.0, - "tool_precision": 1.0, - "tool_f1": 1.0, - "keyword_coverage": 0.8, - "total_time": 22.87535786628723, - "tools_called": [ - "get_customer_detail" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted their account information updated with a new email and phone number. The agent explained the secure update process and requested the new details, clearly outlining next steps. While the update isn't completed yet, the response effectively moves towards resolution.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant correctly understood the user's intent to update their email and phone number and provided a clear explanation of the necessary workflow for such updates, including security and verification steps. It did not attempt any unauthorized or real-world action without corroboration from tools nor expose any sensitive information. No strict presentation or workflow requirements were violated, and there was no ambiguity affecting correctness or safety. The response was complete and actionable for the user's request.", - "llm_tool_score": 2.0, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response effectively helps the customer update their contact information by locating the account, requesting the new email and phone number, and explaining the verification and update process. It addresses security concerns and outlines the next steps clearly. However, it does not explicitly show the current contact information, nor does it mention how the update will affect notifications, billing alerts, password resets, or communication preferences, as specified in the ground truth solution for a perfect score. Thus, while the response is thorough and accurate, it falls short of the \"Excellent\" criteria and earns a \"Good\" rating.", - "llm_eval_time": 29.621996641159058 - }, - { - "scenario": "account_paperless_billing", - "scenario_name": "Paperless Billing Setup", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.8, - "total_time": 27.785822868347168, - "tools_called": [ - "search_knowledge_base" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to stop receiving paper bills and switch to paperless billing. The agent provided clear, step-by-step instructions for enabling paperless billing online, explained the effects, and offered further assistance, fully resolving the intent with thoroughness and accuracy.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant correctly understood the user's goal of stopping paper bills and provided clear, actionable steps for enabling paperless billing on the Contoso portal, including signing in, navigating to billing settings, enabling paperless billing, and confirming the email address. No external actions were claimed\u2014they were suggested as steps the user should take. The assistant did not violate any safety, privacy, or authorization rules, and there were no explicit presentation or workflow constraints specified in the instructions. No evidence of material omission, ambiguity, or incorrect workflow was found.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response provides clear instructions for enabling paperless billing, including steps to check and confirm the email address, and explains the benefits and effects of switching (e.g., bills sent to email, paper bills stop next cycle, online access). It offers to help verify the email and guide the customer further if needed, which is proactive. However, the agent does not directly check or change the customer's billing preferences or confirm the email address themselves; instead, they instruct the customer on how to do it. This falls short of the \"Excellent\" rubric level, which requires the agent to actively check settings and enable paperless billing, but it fully meets the \"Good\" criteria by enabling paperless and confirming changes.", - "llm_eval_time": 27.071513891220093 - }, - { - "scenario": "tv_channel_lineup", - "scenario_name": "TV Channel Lineup Question", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.5, - "total_time": 13.11019253730774, - "tools_called": [ - "get_customer_detail", - "get_product_detail" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know which TV channels are included in their streaming plan. The agent checked the account, clarified that no TV or streaming channels are included, and offered relevant next steps, fully and accurately resolving the intent.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant correctly identified the user's plan and addressed the query about TV streaming channels by informing the user that their current plan does not include TV or streaming services. It provided relevant next steps without fabricating any channel line-up or plan details and did not perform any unauthorized or risky actions. There are no violations of goal achievement, safety, privacy, or required procedural steps noted based on the provided content and absence of tool interactions.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response correctly checks the customer's current subscription and identifies that it does not include TV or streaming services, which is accurate and relevant. However, it does not provide specific details about the available TV streaming plans, their features, or pricing, as required for a higher score. While the agent offers to help the customer explore TV or streaming options and mentions the possibility of adding a subscription, it does not proactively list the plan details or features. According to the rubric, this response provides plan information but lacks the explicit explanation of included channels/features and upgrade options, so it merits a score of 3 (Adequate).", - "llm_eval_time": 29.704005241394043 - }, - { - "scenario": "tv_add_sports", - "scenario_name": "Add Sports Package", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 1.0, - "tool_f1": 0.5, - "keyword_coverage": 0.75, - "total_time": 24.4409658908844, - "tools_called": [ - "get_products" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to add a sports package to watch football games. The agent clearly stated that no sports-specific packages are available, offered to review current subscriptions and check for promotions, and asked clarifying questions to provide alternatives. The intent is mostly resolved, with minor gaps in proactive recommendations.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant responded to the user's query by explaining that no sports-specific packages or add-ons are available, which aligns with the lack of relevant tool calls (none present). The assistant then offered to review current subscriptions and check for eligible promotions, and asked clarifying questions to resolve any ambiguity about the user's preferences, which is an appropriate next step given the user's general request. There is no evidence of rule, safety, privacy, or workflow failures; the assistant did not claim any unverifiable or unsupported actions, nor did it ignore explicit requirements. The content is relevant and complete based on the user's request and the information available. No material failures present.", - "llm_tool_score": 2.0, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is poor because it does not provide accurate information about the availability of sports channels, incorrectly stating that no sports-specific packages or add-ons are offered. It fails to mention the Premium plan, which includes sports channels as per the ground truth solution. While the agent does offer to review the customer's current subscriptions and check for promotions, it does not check the customer's current plan or explain the upgrade option to Premium, nor does it provide any pricing details. The response is generic and does not address the customer's actual request for sports channels, missing key facts and solutions required for a higher score.", - "llm_eval_time": 34.3643479347229 - }, - { - "scenario": "bundle_inquiry", - "scenario_name": "Bundle Package Inquiry", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.6, - "total_time": 14.316256284713745, - "tools_called": [ - "get_customer_detail", - "get_eligible_promotions" - ], - "llm_intent_score": 3.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know if bundling their internet and mobile services would save money. The agent explained general bundle benefits and offered to provide exact savings if more info is given, but did not provide a specific answer or estimate, leaving the intent only partially resolved.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant accurately responded to the user's inquiry regarding potential savings from bundling internet and mobile services. It explained that bundling usually saves money, provided general benefits for bundled plans, and outlined next steps to give a more precise comparison depending on the user's situation. The assistant did not make unverifiable claims about the user's mobile subscription or take unauthorized actions, and no safety, privacy, or presentation contract violations are evident. The lack of tool interactions is appropriate since no external actions or data verification were required. There are no material failures across the criteria.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response provides general information about the benefits of bundling and acknowledges the customer's current internet subscription, but it does not specify the actual bundle option (Family Complete: $199.99/month), the included services, or the 20% discount compared to individual services. It also does not calculate or estimate the customer's current cost or potential savings, nor does it clearly explain how the bundle would offer more than the customer's current services. While the agent offers to help further if the customer provides more information, the response falls short of the \"Good\" and \"Excellent\" criteria in the rubric, as it does not show the value proposition or offer to switch to the bundle. It meets the \"Adequate\" level by providing bundle information and next steps, but lacks the specific details and calculations required for a higher score.", - "llm_eval_time": 31.207448482513428 - }, - { - "scenario": "promotion_eligibility", - "scenario_name": "Promotion Eligibility Check", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.6, - "total_time": 10.153638124465942, - "tools_called": [ - "get_eligible_promotions" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "The user wanted to know if they were eligible for any promotions or discounts. The agent checked the account, confirmed no current eligibility, and offered relevant next steps, fully resolving the intent with thoroughness and clarity.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant clearly understood the user's intent, which was to check for any promotions or discounts available for customer 42. The response directly answers the query, stating that there are no active promotions or discounts currently available, and provides relevant next steps and suggestions without unnecessary tangents. No claims of external actions or data are made that lack corroboration from tool interactions, and there are no procedural, safety, or privacy issues manifest in the response. The assistant does not violate any explicit instructions or constraints, and there are no strict format or presentation requirements to be concerned about in this input.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is poor because it provides a generic statement about promotions without checking or referencing the customer's specific eligibility criteria. While the agent mentions that there are no active promotions for customer ID 42, they do not indicate whether they checked loyalty level, number of active services, or other relevant factors as outlined in the ground truth solution. The response does not list available promotions or explain how the customer could qualify for them, only suggesting general actions like checking back later or asking about upgrades. This falls short of identifying which promotions the customer might qualify for and does not explain how to take advantage of any offers, thus meeting the rubric's definition of a score of 2.", - "llm_eval_time": 27.080150842666626 - }, - { - "scenario": "loyalty_benefits", - "scenario_name": "Loyalty Program Benefits", - "success": true, - "tool_recall": 0.0, - "tool_precision": 0.0, - "tool_f1": 0.0, - "keyword_coverage": 0.5, - "total_time": 12.235161066055298, - "tools_called": [ - "get_eligible_promotions" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know what loyalty benefits they receive as a long-term customer. The agent checked the account, clearly stated no current benefits, explained why, and offered relevant next steps, fully resolving the intent with thoroughness and transparency.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant correctly addressed the user's inquiry about loyalty benefits by checking the system for promotions linked to customer 48 and accurately reported that none were available, without claiming unverifiable or external tool results. The response appropriately explained typical eligibility criteria and offered useful next steps, maintaining safe and respectful communication. No constraints or procedural requirements were violated, and there was no evidence of missing deliverables, unauthorized actions, or privacy concerns. The output is fully usable and meets the user's intent.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 5.0, - "llm_relevance": 5.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response provides generic information about how promotions and loyalty benefits work, but does not check or mention the customer's specific loyalty tier, nor does it explain the benefits associated with any tier. It also fails to mention the upgrade path or highlight any current promotions related to loyalty tiers, as required by the ground truth solution. While the agent offers to check for promotions and review subscriptions, this is not sufficient to meet the higher rubric levels, which require explicit reference to the customer's loyalty status and tier benefits. Therefore, the response fits the \"Poor\" category: generic loyalty info without checking level.", - "llm_eval_time": 23.00485634803772 - }, - { - "scenario": "support_ticket_status", - "scenario_name": "Support Ticket Status Check", - "success": true, - "tool_recall": 0.5, - "tool_precision": 1.0, - "tool_f1": 0.6666666666666666, - "keyword_coverage": 0.8, - "total_time": 10.0857572555542, - "tools_called": [ - "get_support_tickets" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know the status of their support ticket. The agent provided a detailed update, including ticket ID, category, status, dates, and handler, fully resolving the intent. The offer to assist further is a helpful addition.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The user requested a status check on their support ticket as customer 6. The assistant provided a clear update with the ticket ID, category, priority, subject, status (closed), relevant dates, and agent. There are no SYSTEM_MESSAGE constraints restricting format, presentation, or workflow. There is no evidence of external tool use, but the assistant does not claim real-world actions outside its scope\u2014only presents static information. No privacy or security issues are present, and the assistant did not refuse or fabricate capabilities. The answer is relevant, complete, and usable for the user's objective.", - "llm_tool_score": 5.0, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response provides the ticket status, including the ticket ID, category, priority, subject, status, dates, and handler, which aligns with steps 1 and 2 of the ground truth solution. It also explains that the ticket was resolved and closed, addressing step 3. The agent offers to reopen or create a new ticket if the issue persists, which is an appropriate next step. However, since the ticket is closed, there is no explanation of the resolution process or expected timeline, nor is there an offer to escalate or provide urgent contact information as would be required if the ticket were pending. The response is thorough and customer-focused but falls slightly short of the \"Excellent\" criteria, earning a score of 4 for providing status and explanation with a clear next step.", - "llm_eval_time": 31.579761505126953 - }, - { - "scenario": "support_new_ticket", - "scenario_name": "Create New Support Ticket", - "success": true, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.6, - "total_time": 13.643887281417847, - "tools_called": [ - "get_customer_detail", - "create_support_ticket" - ], - "llm_intent_score": 5.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted someone to investigate their cable box's random reboots. The agent confirmed account details and created a technical support ticket, outlining next steps and providing ticket info. The response is thorough and fully resolves the intent.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The assistant said it had created a technical support ticket for the user's cable box issue, implying a real-world action. However, there is no supporting evidence of this in the tool interactions, which are empty. The assistant's output is thus misleading in a material way because the user may expect technician follow-up, but no such action was actually initiated. This directly fails to achieve the user's goal and misrepresents procedural correctness regarding external actions. There is no indication of harmful content, privacy violation, or strict formatting requirements failures.", - "llm_tool_score": null, - "llm_coherence": 5.0, - "llm_fluency": 4.0, - "llm_relevance": 5.0, - "llm_solution_score": 4.0, - "llm_solution_reason": "The agent's response documents the issue (random cable box reboots), confirms the customer's subscription, and creates a support ticket with relevant details (issue description, priority, status, ticket ID). It also outlines next steps, including possible remote diagnostics, contact for troubleshooting, and the option to schedule a technician visit if needed. However, the response does not mention any basic troubleshooting steps (such as unplugging the box or checking connections) before creating the ticket, which is a key action in the ground truth solution. Therefore, while the response is thorough in ticket creation and resolution options, it misses the initial troubleshooting step, resulting in a score of 4.", - "llm_eval_time": 26.090775966644287 - }, - { - "scenario": "multi_billing_dispute", - "scenario_name": "[Multi-Turn] Billing Dispute Resolution", - "success": true, - "tool_recall": 0.6666666666666666, - "tool_precision": 1.0, - "tool_f1": 0.8, - "keyword_coverage": 1.0, - "total_time": 49.481388092041016, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 3.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User wanted to know the reason for a $50 charge on their bill. The agent asked for account details, which is a necessary step to investigate the charge, but then shifted focus to promotions and discounts, partially distracting from the main intent.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user's goal was to understand a $50 charge on their bill. The assistant did not directly address this query, but instead asked for confirmation of the account and offered to review promotions and discounts, which is not relevant or responsive to the question about the charge. This represents a material failure to adhere to the user's objective and is distracting, as the offer to review promotions does not contribute toward resolving the user's concern. There are no privacy or procedural violations evident, since no unauthorized or harmful actions were taken and no sensitive information was exposed.", - "llm_tool_score": 2.0, - "llm_coherence": 3.0, - "llm_fluency": 4.0, - "llm_relevance": 3.0, - "llm_solution_score": 2.0, - "llm_solution_reason": "The agent's response is poor because it does not address the billing dispute or investigate the $50 charge as required in Turn 1 of the ground truth solution. Instead, it immediately asks for account information and offers to review promotions, skipping the crucial step of identifying and explaining the disputed charge. There is no mention of handling a credit request or confirming any billing adjustment, which are essential for Turn 2. While the agent does mention reviewing promotions (Turn 3), it is generic and not personalized to the customer's situation. Overall, the response misses context between turns and provides a generic reply, failing to address the customer's actual needs regarding the billing dispute.", - "llm_eval_time": 34.14159631729126 - }, - { - "scenario": "multi_internet_troubleshoot", - "scenario_name": "[Multi-Turn] Internet Troubleshooting Flow", - "success": true, - "tool_recall": 0.5, - "tool_precision": 0.6666666666666666, - "tool_f1": 0.5714285714285715, - "keyword_coverage": 0.8333333333333334, - "total_time": 69.60780620574951, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail", - "create_support_ticket" - ], - "llm_intent_score": 4.0, - "llm_intent_result": "pass", - "llm_intent_reason": "User reported frequent internet drops and expressed frustration. The agent responded by offering technician appointment options and requested necessary details to proceed, effectively moving towards resolving the issue. However, it did not offer immediate troubleshooting or acknowledge the user's frustration, but the resolution path is clear.", - "llm_task_score": 1.0, - "llm_task_result": "pass", - "llm_task_reason": "The assistant recognized the user's internet connectivity issue and provided relevant information about technician appointment availability. It asked for essential details needed to proceed (customer ID, service address, issue description, preferred appointment window) and did not make any unverifiable claims or take external actions without corroboration. There were no violations of safety, privacy, or other explicit rules. The assistant did not skip required steps and followed a reasonable workflow by eliciting additional relevant information before proceeding. The response is appropriate, on-topic, and fully usable.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 4.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response focuses solely on scheduling a technician appointment, which aligns with Turn 4 of the ground truth solution. However, it skips the earlier troubleshooting steps, such as checking service status, acknowledging previous troubleshooting attempts, and suggesting additional steps before escalation. While the response provides accurate information about appointment windows and requests necessary details to book the visit, it lacks continuity and progression from initial diagnosis through troubleshooting to escalation. Therefore, it is adequate but does not fully follow the multi-turn flow expected in the rubric.", - "llm_eval_time": 28.264445543289185 - }, - { - "scenario": "multi_service_cancellation", - "scenario_name": "[Multi-Turn] Service Cancellation Retention", - "success": false, - "tool_recall": 0.25, - "tool_precision": 0.5, - "tool_f1": 0.3333333333333333, - "keyword_coverage": 0.8333333333333334, - "total_time": 55.04346513748169, - "tools_called": [ - "get_customer_detail", - "update_subscription" - ], - "llm_intent_score": 2.0, - "llm_intent_result": "fail", - "llm_intent_reason": "User wanted to cancel their internet service due to high cost. The agent instead offered a discount and requested more information to apply it, but did not address the cancellation request directly, leaving the core intent unresolved.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user's explicit request was to cancel their internet service because it's too expensive. The assistant did not acknowledge the cancellation request nor provide any steps or confirmation toward service cancellation. Instead, it responded with a retention offer and requested account information to apply a discount. There is no evidence that the user's goal (cancellation) was addressed; this is a material failure in meeting the user's objective. There is no indication of safety, privacy, workflow, or procedural violations, but the core goal was not met.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 3.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response attempts retention by offering a 20% discount and explains how the new rate would be calculated, which aligns with part of the ground truth solution. However, it lacks empathy and does not address the customer's specific cancellation reason or needs, missing the important first step of understanding and personalizing the retention effort. The response is transactional and does not highlight value-adds or acknowledge competitor pricing, which are key elements for a strong retention attempt. While it provides accurate information and a clear next step, it falls short of the ideal multi-turn, empathetic retention flow described in the rubric.", - "llm_eval_time": 27.327298402786255 - }, - { - "scenario": "multi_new_customer_setup", - "scenario_name": "[Multi-Turn] New Service Setup Assistance", - "success": false, - "tool_recall": 0.3333333333333333, - "tool_precision": 0.5, - "tool_f1": 0.4, - "keyword_coverage": 0.7142857142857143, - "total_time": 52.34556484222412, - "tools_called": [ - "get_products", - "get_promotions" - ], - "llm_intent_score": 2.0, - "llm_intent_result": "fail", - "llm_intent_reason": "The user wanted to know their internet setup options after moving to a new apartment. The agent prematurely assumed the Pro plan and new customer discount, rather than presenting available options, leaving the core intent unresolved.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user requested information about internet options for a new apartment. The assistant instead assumed the user wanted to be set up on a specific 'Pro plan' and started a workflow that depended on verifying the user's account details, without presenting an overview of available internet options or allowing the user to choose. The assistant did not fulfill the user's actual objective of learning about different internet plans or choices, but rather assumed a conversion into a specific plan. No privacy or procedural errors occurred, but the core task (informing the user of their options) was not achieved.", - "llm_tool_score": null, - "llm_coherence": 4.0, - "llm_fluency": 4.0, - "llm_relevance": 3.0, - "llm_solution_score": 3.0, - "llm_solution_reason": "The agent's response is adequate in that it initiates the setup process and requests necessary information to proceed, but it lacks several key elements from the ground truth solution. It does not present the available internet plans, explain speed tiers or pricing, or ask about the customer's usage needs, which means it misses the opportunity for a personalized recommendation. While it mentions applying the new customer discount and setting up the Pro plan, it does not specify the discount amount, the plan's features, or the installation process. The response is functional but misses important steps for a natural sales flow and a complete, personalized setup experience, as outlined in the rubric.", - "llm_eval_time": 26.868063926696777 - }, - { - "scenario": "multi_complex_account_issue", - "scenario_name": "[Multi-Turn] Complex Account Resolution", - "success": false, - "tool_recall": 0.4, - "tool_precision": 1.0, - "tool_f1": 0.5714285714285715, - "keyword_coverage": 0.75, - "total_time": 62.33300185203552, - "tools_called": [ - "get_customer_detail", - "get_subscription_detail" - ], - "llm_intent_score": 1.0, - "llm_intent_result": "fail", - "llm_intent_reason": "User wanted help with being charged for a cancelled service. The agent did not address the billing issue, offer a review, or request relevant details, instead giving a generic response about account changes. The intent was ignored, resulting in a very poor resolution.", - "llm_task_score": 0.0, - "llm_task_result": "fail", - "llm_task_reason": "The user reported being charged for a service they cancelled last month and expected assistance with this specific issue. The assistant did not address the user's core concern, instead giving a generic statement about no changes being made and requesting customer ID or more details, despite already being provided the customer identifier 'customer 11'. This omits necessary next steps and fails to investigate or acknowledge the billing issue. There are no safety, privacy, or procedural violations, but the output does not meaningfully progress or respond to the user's stated objective. Therefore, this is a material failure in adhering to the user's goal.", - "llm_tool_score": 2.0, - "llm_coherence": 3.0, - "llm_fluency": 4.0, - "llm_relevance": 2.0, - "llm_solution_score": 1.0, - "llm_solution_reason": "The agent's response fails to address any of the customer's three issues (billing, internet, TV downgrade) as outlined in the ground truth solution. It does not identify the root causes, provide accurate information, or offer any solutions or next steps. Instead, it asks the customer to clarify what changes were expected, rather than proactively reviewing the account or resolving the stated problems. The response does not maintain context or demonstrate awareness of multiple issues, and it does not provide a summary or confirmation of resolution. According to the rubric, this is a \"Fail\" as the agent is unable to handle multiple issues or forgets earlier requests.", - "llm_eval_time": 38.21580767631531 - } - ] -} \ No newline at end of file diff --git a/tests/evaluation/agent_evaluator.py b/tests/evaluation/agent_evaluator.py deleted file mode 100644 index 962385e94..000000000 --- a/tests/evaluation/agent_evaluator.py +++ /dev/null @@ -1,658 +0,0 @@ -""" -Agent Evaluation Module -======================= -Comprehensive evaluation framework for AI Agents using Azure AI Evaluation SDK. - -This module provides: -- AgentRunner: Collects responses from the agent for test datasets -- AgentEvaluator: Runs evaluations using Azure AI Evaluation SDK -- EvaluationMetrics: Defines metrics and thresholds for agent evaluation - -Metrics evaluated: -- Intent Resolution: Did the agent correctly identify the user's intent? -- Tool Call Accuracy: Did the agent use the correct tools? -- Task Adherence: Did the agent complete the requested task? -- Groundedness: Are the agent's responses grounded in retrieved data? -- Response Quality: Relevance, coherence, and fluency of responses -""" - -import asyncio -import json -import logging -import os -import sys -from dataclasses import dataclass, field -from datetime import datetime -from pathlib import Path -from typing import Any, Dict, List, Optional - -from dotenv import load_dotenv - -# Add parent directories to path for imports -sys.path.insert(0, str(Path(__file__).parent.parent.parent / "agentic_ai" / "applications")) -sys.path.insert(0, str(Path(__file__).parent.parent.parent / "agentic_ai")) - -load_dotenv() - -logger = logging.getLogger(__name__) - - -@dataclass -class EvaluationThresholds: - """Configurable thresholds for evaluation metrics.""" - intent_resolution: float = 0.8 - tool_call_accuracy: float = 0.5 # Lower threshold - agent may use subset of expected tools - task_adherence: float = 0.8 - groundedness: float = 0.7 - relevance: float = 0.8 - coherence: float = 0.8 - fluency: float = 0.8 - - -@dataclass -class TestCase: - """A single test case for agent evaluation.""" - query: str - customer_id: str - expected_intent: str - expected_tools: List[str] - ground_truth: str - category: str - complexity: str - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "TestCase": - return cls( - query=data["query"], - customer_id=data["customer_id"], - expected_intent=data["expected_intent"], - expected_tools=data["expected_tools"], - ground_truth=data["ground_truth"], - category=data["category"], - complexity=data["complexity"], - ) - - -@dataclass -class AgentResponse: - """Captured response from the agent.""" - test_case: TestCase - response: str - tools_called: List[str] = field(default_factory=list) - execution_time_ms: float = 0.0 - error: Optional[str] = None - - -class AgentRunner: - """ - Runs the agent against test cases and collects responses. - Supports both single agent and multi-agent patterns. - """ - - def __init__(self, agent_module: str = "agents.agent_framework.single_agent"): - """ - Initialize the agent runner. - - Args: - agent_module: Module path for the agent to test - """ - self.agent_module = agent_module - self._agent_class = None - self._state_store: Dict[str, Any] = {} - - def _load_agent_class(self): - """Dynamically load the agent class.""" - if self._agent_class is not None: - return - - import importlib - module = importlib.import_module(self.agent_module) - self._agent_class = getattr(module, "Agent") - logger.info(f"Loaded agent class from {self.agent_module}") - - async def run_single_test(self, test_case: TestCase, session_id: Optional[str] = None) -> AgentResponse: - """ - Run a single test case through the agent. - - Args: - test_case: The test case to run - session_id: Optional session ID (generates unique one if not provided) - - Returns: - AgentResponse with the agent's response and metadata - """ - import time - - self._load_agent_class() - - if session_id is None: - session_id = f"eval_{test_case.customer_id}_{int(time.time() * 1000)}" - - # Prepare the prompt with customer context - if test_case.customer_id and test_case.customer_id not in test_case.query.lower(): - prompt = f"Customer {test_case.customer_id}: {test_case.query}" - else: - prompt = test_case.query - - start_time = time.time() - tools_called: List[str] = [] - error: Optional[str] = None - response: str = "" - - try: - # Create agent instance - agent = self._agent_class( - state_store=self._state_store, - session_id=session_id, - access_token=None, - ) - - # Inject a tool call tracker if the agent supports WebSocket manager - tool_tracker = ToolCallTracker() - if hasattr(agent, 'set_websocket_manager'): - agent.set_websocket_manager(tool_tracker) - - # Run the agent - response = await agent.chat_async(prompt) - tools_called = tool_tracker.get_tools_called() - - except Exception as e: - error = str(e) - logger.error(f"Error running test case: {e}") - - execution_time_ms = (time.time() - start_time) * 1000 - - return AgentResponse( - test_case=test_case, - response=response, - tools_called=tools_called, - execution_time_ms=execution_time_ms, - error=error, - ) - - async def run_test_dataset(self, test_cases: List[TestCase], max_concurrent: int = 1) -> List[AgentResponse]: - """ - Run all test cases through the agent. - - Args: - test_cases: List of test cases to run - max_concurrent: Maximum concurrent test runs (default 1 for deterministic results) - - Returns: - List of AgentResponse objects - """ - responses = [] - - for i, test_case in enumerate(test_cases): - logger.info(f"Running test case {i+1}/{len(test_cases)}: {test_case.query[:50]}...") - response = await self.run_single_test(test_case) - responses.append(response) - - # Clear state between tests for independent evaluation - self._state_store.clear() - - return responses - - -class ToolCallTracker: - """ - Mock WebSocket manager that tracks tool calls. - Used to capture which tools the agent calls during execution. - """ - - def __init__(self): - self._tools_called: List[str] = [] - - async def broadcast(self, session_id: str, message: Dict[str, Any]) -> None: - """Capture tool call events from the agent.""" - if message.get("type") == "tool_called": - tool_name = message.get("tool_name") - if tool_name and tool_name not in self._tools_called: - self._tools_called.append(tool_name) - - def get_tools_called(self) -> List[str]: - """Return the list of tools that were called.""" - return self._tools_called.copy() - - -class AgentEvaluator: - """ - Evaluates agent responses using Azure AI Evaluation SDK. - - Supports multiple evaluation types: - - AI-assisted evaluation (requires Azure OpenAI) - - Rule-based evaluation (no external dependencies) - """ - - def __init__( - self, - azure_endpoint: Optional[str] = None, - azure_deployment: Optional[str] = None, - api_version: Optional[str] = None, - thresholds: Optional[EvaluationThresholds] = None, - ): - """ - Initialize the evaluator. - - Args: - azure_endpoint: Azure OpenAI endpoint for AI-assisted evaluation - azure_deployment: Azure OpenAI deployment name - api_version: Azure OpenAI API version - thresholds: Evaluation thresholds - """ - self.azure_endpoint = azure_endpoint or os.getenv("AZURE_OPENAI_ENDPOINT") - self.azure_deployment = azure_deployment or os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT") - self.api_version = api_version or os.getenv("AZURE_OPENAI_API_VERSION") - self.thresholds = thresholds or EvaluationThresholds() - - self._ai_evaluators_available = False - self._init_ai_evaluators() - - def _init_ai_evaluators(self): - """Initialize Azure AI Evaluation SDK evaluators if available.""" - try: - from azure.ai.evaluation import ( - RelevanceEvaluator, - CoherenceEvaluator, - FluencyEvaluator, - GroundednessEvaluator, - ) - - if self.azure_endpoint and self.azure_deployment: - model_config = { - "azure_endpoint": self.azure_endpoint, - "azure_deployment": self.azure_deployment, - "api_version": self.api_version, - } - - # Check if API key is available - api_key = os.getenv("AZURE_OPENAI_API_KEY") - if api_key: - model_config["api_key"] = api_key - - self._relevance_evaluator = RelevanceEvaluator(model_config=model_config) - self._coherence_evaluator = CoherenceEvaluator(model_config=model_config) - self._fluency_evaluator = FluencyEvaluator(model_config=model_config) - self._groundedness_evaluator = GroundednessEvaluator(model_config=model_config) - - self._ai_evaluators_available = True - logger.info("Azure AI Evaluation SDK evaluators initialized successfully") - - except ImportError: - logger.warning("Azure AI Evaluation SDK not installed. Using rule-based evaluation only.") - except Exception as e: - logger.warning(f"Failed to initialize AI evaluators: {e}. Using rule-based evaluation only.") - - def evaluate_tool_accuracy(self, response: AgentResponse) -> Dict[str, Any]: - """ - Evaluate tool call accuracy. - - Computes: - - Precision: What fraction of called tools were expected? - - Recall: What fraction of expected tools were called? - - F1 Score: Harmonic mean of precision and recall - """ - expected = set(response.test_case.expected_tools) - called = set(response.tools_called) - - if len(called) == 0: - precision = 0.0 - else: - precision = len(expected & called) / len(called) - - if len(expected) == 0: - recall = 1.0 - else: - recall = len(expected & called) / len(expected) - - if precision + recall == 0: - f1_score = 0.0 - else: - f1_score = 2 * (precision * recall) / (precision + recall) - - return { - "tool_precision": precision, - "tool_recall": recall, - "tool_f1_score": f1_score, - "expected_tools": list(expected), - "called_tools": list(called), - "missing_tools": list(expected - called), - "extra_tools": list(called - expected), - "passed": f1_score >= self.thresholds.tool_call_accuracy, - } - - def evaluate_response_quality(self, response: AgentResponse) -> Dict[str, Any]: - """ - Evaluate basic response quality using rule-based checks. - """ - text = response.response - - # Check for empty or error responses - if not text or response.error: - return { - "has_content": False, - "length": 0, - "has_error": bool(response.error), - "error_message": response.error, - "passed": False, - } - - # Basic quality checks - word_count = len(text.split()) - sentence_count = text.count('.') + text.count('!') + text.count('?') - - # Check for hallucination indicators - hallucination_phrases = [ - "i don't have access", - "i cannot actually", - "as an ai, i cannot", - "i apologize, but i cannot", - ] - has_hallucination_warning = any(phrase in text.lower() for phrase in hallucination_phrases) - - return { - "has_content": True, - "word_count": word_count, - "sentence_count": sentence_count, - "has_hallucination_warning": has_hallucination_warning, - "execution_time_ms": response.execution_time_ms, - "passed": word_count > 10 and not has_hallucination_warning, - } - - async def evaluate_with_ai(self, response: AgentResponse) -> Dict[str, Any]: - """ - Evaluate response using Azure AI Evaluation SDK evaluators. - - Returns AI-assisted scores for: - - Relevance - - Coherence - - Fluency - - Groundedness - """ - if not self._ai_evaluators_available: - return {"ai_evaluation": "unavailable", "reason": "AI evaluators not initialized"} - - query = response.test_case.query - answer = response.response - context = response.test_case.ground_truth - - results = {} - - try: - # Relevance evaluation - relevance_result = self._relevance_evaluator( - query=query, - response=answer, - ) - results["relevance"] = relevance_result.get("relevance", 0) - results["relevance_passed"] = results["relevance"] >= self.thresholds.relevance * 5 # SDK uses 1-5 scale - - except Exception as e: - logger.warning(f"Relevance evaluation failed: {e}") - results["relevance_error"] = str(e) - - try: - # Coherence evaluation - coherence_result = self._coherence_evaluator( - query=query, - response=answer, - ) - results["coherence"] = coherence_result.get("coherence", 0) - results["coherence_passed"] = results["coherence"] >= self.thresholds.coherence * 5 - - except Exception as e: - logger.warning(f"Coherence evaluation failed: {e}") - results["coherence_error"] = str(e) - - try: - # Fluency evaluation - fluency_result = self._fluency_evaluator( - query=query, - response=answer, - ) - results["fluency"] = fluency_result.get("fluency", 0) - results["fluency_passed"] = results["fluency"] >= self.thresholds.fluency * 5 - - except Exception as e: - logger.warning(f"Fluency evaluation failed: {e}") - results["fluency_error"] = str(e) - - try: - # Groundedness evaluation - groundedness_result = self._groundedness_evaluator( - query=query, - response=answer, - context=context, - ) - results["groundedness"] = groundedness_result.get("groundedness", 0) - results["groundedness_passed"] = results["groundedness"] >= self.thresholds.groundedness * 5 - - except Exception as e: - logger.warning(f"Groundedness evaluation failed: {e}") - results["groundedness_error"] = str(e) - - return results - - async def evaluate_response(self, response: AgentResponse, include_ai_eval: bool = True) -> Dict[str, Any]: - """ - Run full evaluation on an agent response. - - Args: - response: The agent response to evaluate - include_ai_eval: Whether to include AI-assisted evaluation - - Returns: - Dictionary with all evaluation results - """ - results = { - "test_case": { - "query": response.test_case.query, - "customer_id": response.test_case.customer_id, - "expected_intent": response.test_case.expected_intent, - "category": response.test_case.category, - "complexity": response.test_case.complexity, - }, - "response_preview": response.response[:500] if response.response else None, - "execution_time_ms": response.execution_time_ms, - "error": response.error, - } - - # Tool accuracy evaluation - results["tool_accuracy"] = self.evaluate_tool_accuracy(response) - - # Response quality evaluation - results["response_quality"] = self.evaluate_response_quality(response) - - # AI-assisted evaluation - if include_ai_eval and self._ai_evaluators_available: - results["ai_evaluation"] = await self.evaluate_with_ai(response) - - # Overall pass/fail - tool_passed = results["tool_accuracy"]["passed"] - quality_passed = results["response_quality"]["passed"] - - results["passed"] = tool_passed and quality_passed - - return results - - async def evaluate_all( - self, - responses: List[AgentResponse], - include_ai_eval: bool = True, - ) -> Dict[str, Any]: - """ - Evaluate all responses and generate summary statistics. - - Args: - responses: List of agent responses to evaluate - include_ai_eval: Whether to include AI-assisted evaluation - - Returns: - Dictionary with individual results and summary statistics - """ - individual_results = [] - - for i, response in enumerate(responses): - logger.info(f"Evaluating response {i+1}/{len(responses)}...") - result = await self.evaluate_response(response, include_ai_eval) - individual_results.append(result) - - # Compute summary statistics - total = len(individual_results) - passed = sum(1 for r in individual_results if r["passed"]) - - tool_f1_scores = [r["tool_accuracy"]["tool_f1_score"] for r in individual_results] - avg_tool_f1 = sum(tool_f1_scores) / total if total > 0 else 0 - - exec_times = [r["execution_time_ms"] for r in individual_results] - avg_exec_time = sum(exec_times) / total if total > 0 else 0 - - # Category breakdown - categories = {} - for result in individual_results: - cat = result["test_case"]["category"] - if cat not in categories: - categories[cat] = {"total": 0, "passed": 0} - categories[cat]["total"] += 1 - if result["passed"]: - categories[cat]["passed"] += 1 - - summary = { - "total_tests": total, - "passed": passed, - "failed": total - passed, - "pass_rate": passed / total if total > 0 else 0, - "average_tool_f1_score": avg_tool_f1, - "average_execution_time_ms": avg_exec_time, - "category_breakdown": categories, - "thresholds": { - "tool_call_accuracy": self.thresholds.tool_call_accuracy, - "groundedness": self.thresholds.groundedness, - "relevance": self.thresholds.relevance, - }, - } - - return { - "summary": summary, - "individual_results": individual_results, - "timestamp": datetime.utcnow().isoformat(), - } - - -def load_test_data(file_path: str) -> List[TestCase]: - """ - Load test cases from a JSONL file. - - Args: - file_path: Path to the JSONL file - - Returns: - List of TestCase objects - """ - test_cases = [] - - with open(file_path, 'r', encoding='utf-8') as f: - for line in f: - line = line.strip() - if line: - data = json.loads(line) - test_cases.append(TestCase.from_dict(data)) - - return test_cases - - -def save_evaluation_results(results: Dict[str, Any], output_path: str) -> None: - """ - Save evaluation results to a JSON file. - - Args: - results: Evaluation results dictionary - output_path: Path to save the results - """ - with open(output_path, 'w', encoding='utf-8') as f: - json.dump(results, f, indent=2, default=str) - - logger.info(f"Evaluation results saved to {output_path}") - - -async def run_evaluation( - test_data_path: str, - agent_module: str = "agents.agent_framework.single_agent", - output_path: Optional[str] = None, - include_ai_eval: bool = True, -) -> Dict[str, Any]: - """ - Run a complete evaluation pipeline. - - Args: - test_data_path: Path to test data JSONL file - agent_module: Module path for the agent to test - output_path: Optional path to save results - include_ai_eval: Whether to include AI-assisted evaluation - - Returns: - Evaluation results dictionary - """ - logger.info(f"Loading test data from {test_data_path}") - test_cases = load_test_data(test_data_path) - logger.info(f"Loaded {len(test_cases)} test cases") - - # Run agent against test cases - logger.info(f"Running agent: {agent_module}") - runner = AgentRunner(agent_module=agent_module) - responses = await runner.run_test_dataset(test_cases) - logger.info(f"Collected {len(responses)} responses") - - # Evaluate responses - logger.info("Evaluating responses...") - evaluator = AgentEvaluator() - results = await evaluator.evaluate_all(responses, include_ai_eval=include_ai_eval) - - # Save results if output path provided - if output_path: - save_evaluation_results(results, output_path) - - # Print summary - summary = results["summary"] - print("\n" + "="*60) - print("EVALUATION SUMMARY") - print("="*60) - print(f"Total Tests: {summary['total_tests']}") - print(f"Passed: {summary['passed']}") - print(f"Failed: {summary['failed']}") - print(f"Pass Rate: {summary['pass_rate']:.1%}") - print(f"Avg Tool F1: {summary['average_tool_f1_score']:.2f}") - print(f"Avg Exec Time: {summary['average_execution_time_ms']:.0f}ms") - print("\nBy Category:") - for cat, stats in summary["category_breakdown"].items(): - rate = stats['passed'] / stats['total'] if stats['total'] > 0 else 0 - print(f" {cat}: {stats['passed']}/{stats['total']} ({rate:.0%})") - print("="*60 + "\n") - - return results - - -if __name__ == "__main__": - # Allow running as a standalone script - import argparse - - parser = argparse.ArgumentParser(description="Run agent evaluation") - parser.add_argument("--test-data", default="tests/evaluation/test_data.jsonl", - help="Path to test data JSONL file") - parser.add_argument("--agent-module", default="agents.agent_framework.single_agent", - help="Agent module to test") - parser.add_argument("--output", default=None, - help="Path to save evaluation results") - parser.add_argument("--no-ai-eval", action="store_true", - help="Disable AI-assisted evaluation") - - args = parser.parse_args() - - logging.basicConfig(level=logging.INFO) - - asyncio.run(run_evaluation( - test_data_path=args.test_data, - agent_module=args.agent_module, - output_path=args.output, - include_ai_eval=not args.no_ai_eval, - )) diff --git a/tests/evaluation/agent_runner.py b/tests/evaluation/agent_runner.py deleted file mode 100644 index 81efb1495..000000000 --- a/tests/evaluation/agent_runner.py +++ /dev/null @@ -1,376 +0,0 @@ -""" -Generic Agent Evaluation Runner - -This module provides a consistent interface for evaluating ANY agent implementation. -It works with any agent that follows the standard BaseAgent pattern: -1. Inherits from BaseAgent -2. Implements set_websocket_manager(manager) -3. Implements chat_async(prompt) -> str -4. Broadcasts events via _ws_manager.broadcast() - -Usage: - from agent_runner import AgentTestRunner, ToolCallTracker - - # Load any agent by module path - runner = AgentTestRunner("agents.agent_framework.single_agent") - - # Run with tool tracking - result = await runner.run_query("What is customer 251's balance?") - print(result.response) - print(result.tool_calls) -""" - -import asyncio -import importlib -import os -import sys -import time -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any - -from dotenv import load_dotenv - -# ═══════════════════════════════════════════════════════════════════════════════ -# PATH SETUP -# ═══════════════════════════════════════════════════════════════════════════════ - -_eval_dir = Path(__file__).parent.resolve() -_tests_dir = _eval_dir.parent -_workspace_root = _tests_dir.parent -_agentic_ai_dir = _workspace_root / "agentic_ai" - -sys.path.insert(0, str(_agentic_ai_dir)) -sys.path.insert(0, str(_tests_dir)) - -load_dotenv(_eval_dir / ".env") - - -# ═══════════════════════════════════════════════════════════════════════════════ -# TOOL CALL TRACKER -# ═══════════════════════════════════════════════════════════════════════════════ - - -class ToolCallTracker: - """ - Captures agent events by implementing the WebSocket manager interface. - - All agents in this codebase follow a consistent pattern: - 1. Inherit from BaseAgent - 2. Override set_websocket_manager(manager) to store the manager - 3. Call self._ws_manager.broadcast(session_id, message) for events - - This tracker captures those events, providing a consistent testing interface - for ANY agent implementation (current and future). - - Standard Event Types: - - tool_called: {type: "tool_called", tool_name: str, agent_id: str} - - agent_start: {type: "agent_start", agent_id: str, agent_name: str} - - agent_token: {type: "agent_token", agent_id: str, content: str} - - final_result: {type: "final_result", content: str} - """ - - def __init__(self): - self.events: list[dict] = [] - self.tool_calls: list[str] = [] - self.agent_transitions: list[str] = [] # For multi-agent tracking - - async def broadcast(self, session_id: str, message: dict) -> None: - """Capture broadcast messages (implements WebSocket manager interface).""" - self.events.append({"session_id": session_id, "timestamp": time.time(), **message}) - - msg_type = message.get("type", "") - - if msg_type == "tool_called": - tool_name = message.get("tool_name", "") - if tool_name: - self.tool_calls.append(tool_name) - - if msg_type == "agent_start": - agent_id = message.get("agent_id", "unknown") - self.agent_transitions.append(agent_id) - - def get_tool_calls(self) -> list[str]: - """Get list of tools called in order.""" - return self.tool_calls.copy() - - def get_unique_tools(self) -> set[str]: - """Get unique set of tools called.""" - return set(self.tool_calls) - - def get_agent_transitions(self) -> list[str]: - """Get agent transitions (for multi-agent evaluation).""" - return self.agent_transitions.copy() - - def get_events_by_type(self, event_type: str) -> list[dict]: - """Get all events of a specific type.""" - return [e for e in self.events if e.get("type") == event_type] - - def reset(self): - """Reset tracker for new conversation turn.""" - self.events.clear() - self.tool_calls.clear() - self.agent_transitions.clear() - - -# ═══════════════════════════════════════════════════════════════════════════════ -# QUERY RESULT -# ═══════════════════════════════════════════════════════════════════════════════ - - -@dataclass -class QueryResult: - """Result from running a query against an agent.""" - - query: str - response: str - tool_calls: list[str] = field(default_factory=list) - agent_transitions: list[str] = field(default_factory=list) - events: list[dict] = field(default_factory=list) - execution_time: float = 0.0 - error: str | None = None - - @property - def success(self) -> bool: - return self.error is None and bool(self.response) - - @property - def unique_tools(self) -> set[str]: - return set(self.tool_calls) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# AGENT TEST RUNNER -# ═══════════════════════════════════════════════════════════════════════════════ - - -class AgentTestRunner: - """ - Generic test runner for any agent implementation. - - Works with any agent that: - 1. Has an Agent class in the module - 2. Agent.__init__(state_store, session_id, ...) - 3. Agent.set_websocket_manager(manager) - 4. Agent.chat_async(prompt) -> str - - Example: - runner = AgentTestRunner("agents.agent_framework.single_agent") - result = await runner.run_query("Hello") - print(result.tool_calls) - """ - - # Known agent modules for convenience - KNOWN_AGENTS = { - "single": "agents.agent_framework.single_agent", - "reflection": "agents.agent_framework.multi_agent.reflection_agent", - "handoff": "agents.agent_framework.multi_agent.handoff_multi_domain_agent", - "magentic": "agents.agent_framework.multi_agent.magentic_group", - } - - def __init__(self, agent_module: str): - """ - Initialize runner with an agent module path. - - Args: - agent_module: Full module path (e.g., "agents.agent_framework.single_agent") - or shorthand ("single", "reflection", "handoff", "magentic") - """ - # Resolve shorthand names - self.agent_module = self.KNOWN_AGENTS.get(agent_module, agent_module) - self._agent_class = None - self._tracker = ToolCallTracker() - self._session_counter = 0 - - def _load_agent_class(self): - """Dynamically load the Agent class from the module.""" - if self._agent_class is None: - module = importlib.import_module(self.agent_module) - self._agent_class = getattr(module, "Agent") - return self._agent_class - - def _create_agent(self, session_id: str | None = None) -> Any: - """Create a new agent instance with fresh state.""" - AgentClass = self._load_agent_class() - - if session_id is None: - self._session_counter += 1 - session_id = f"eval_{self._session_counter}_{int(time.time() * 1000)}" - - state_store: dict[str, Any] = {} - agent = AgentClass(state_store=state_store, session_id=session_id) - agent.set_websocket_manager(self._tracker) - - return agent - - async def run_query( - self, - query: str, - session_id: str | None = None, - reset_tracker: bool = True, - ) -> QueryResult: - """ - Run a single query against the agent. - - Args: - query: User query to send - session_id: Optional session ID (auto-generated if not provided) - reset_tracker: Whether to reset the tracker before running - - Returns: - QueryResult with response, tool calls, and events - """ - if reset_tracker: - self._tracker.reset() - - agent = self._create_agent(session_id) - start_time = time.time() - - try: - response = await agent.chat_async(query) - - return QueryResult( - query=query, - response=response, - tool_calls=self._tracker.get_tool_calls(), - agent_transitions=self._tracker.get_agent_transitions(), - events=self._tracker.events.copy(), - execution_time=time.time() - start_time, - ) - except Exception as e: - return QueryResult( - query=query, - response="", - tool_calls=self._tracker.get_tool_calls(), - agent_transitions=self._tracker.get_agent_transitions(), - events=self._tracker.events.copy(), - execution_time=time.time() - start_time, - error=str(e), - ) - - async def run_conversation( - self, - queries: list[str], - session_id: str | None = None, - ) -> list[QueryResult]: - """ - Run a multi-turn conversation with the same agent instance. - - Args: - queries: List of user queries in order - session_id: Session ID for conversation continuity - - Returns: - List of QueryResult, one per turn - """ - if session_id is None: - self._session_counter += 1 - session_id = f"conv_{self._session_counter}_{int(time.time() * 1000)}" - - agent = self._create_agent(session_id) - results = [] - - for query in queries: - self._tracker.reset() # Reset per turn - start_time = time.time() - - try: - response = await agent.chat_async(query) - - results.append(QueryResult( - query=query, - response=response, - tool_calls=self._tracker.get_tool_calls(), - agent_transitions=self._tracker.get_agent_transitions(), - events=self._tracker.events.copy(), - execution_time=time.time() - start_time, - )) - except Exception as e: - results.append(QueryResult( - query=query, - response="", - tool_calls=self._tracker.get_tool_calls(), - agent_transitions=self._tracker.get_agent_transitions(), - events=self._tracker.events.copy(), - execution_time=time.time() - start_time, - error=str(e), - )) - - return results - - -# ═══════════════════════════════════════════════════════════════════════════════ -# CONVENIENCE FUNCTIONS -# ═══════════════════════════════════════════════════════════════════════════════ - - -def list_available_agents() -> dict[str, str]: - """List known agent shorthand names and their module paths.""" - return AgentTestRunner.KNOWN_AGENTS.copy() - - -async def compare_agents( - query: str, - agent_modules: list[str] | None = None, -) -> dict[str, QueryResult]: - """ - Run the same query against multiple agents and compare results. - - Args: - query: Query to run - agent_modules: List of agent modules (defaults to all known agents) - - Returns: - Dict mapping agent name to QueryResult - """ - if agent_modules is None: - agent_modules = ["single", "reflection"] - - results = {} - for agent_name in agent_modules: - runner = AgentTestRunner(agent_name) - result = await runner.run_query(query) - results[agent_name] = result - - status = "✅" if result.success else "❌" - print(f"{status} {agent_name}: {result.execution_time:.1f}s, " - f"tools={len(result.tool_calls)}") - - return results - - -# ═══════════════════════════════════════════════════════════════════════════════ -# STANDALONE DEMO -# ═══════════════════════════════════════════════════════════════════════════════ - -if __name__ == "__main__": - import logging - import warnings - - # Suppress MCP client cleanup warnings (they don't affect results) - logging.getLogger("asyncio").setLevel(logging.CRITICAL) - warnings.filterwarnings("ignore", category=DeprecationWarning) - - async def demo(): - print("Agent Test Runner Demo") - print("=" * 50) - - # Show available agents - print("\nAvailable agents:") - for name, module in list_available_agents().items(): - print(f" {name}: {module}") - - # Run a simple comparison - print("\nComparing agents on a simple query...") - query = "Hi, I'm customer 251. Can you tell me about my account?" - - results = await compare_agents(query, ["single", "reflection"]) - - print("\n" + "-" * 50) - for name, result in results.items(): - print(f"\n{name}:") - print(f" Response: {result.response[:150]}...") - print(f" Tools: {result.tool_calls}") - print(f" Time: {result.execution_time:.1f}s") - - asyncio.run(demo()) diff --git a/tests/evaluation/all_agents_comparison.json b/tests/evaluation/all_agents_comparison.json deleted file mode 100644 index 9e26dfeeb..000000000 --- a/tests/evaluation/all_agents_comparison.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/tests/evaluation/llm_judge_evaluator.py b/tests/evaluation/llm_judge_evaluator.py deleted file mode 100644 index 522f81691..000000000 --- a/tests/evaluation/llm_judge_evaluator.py +++ /dev/null @@ -1,759 +0,0 @@ -""" -LLM-as-Judge Evaluator using Azure AI Foundry Evaluation SDK. - -This module provides LLM-based evaluation for AI agents, replacing simple -keyword matching with sophisticated AI-assisted judgment. - -Azure AI Foundry Evaluators Used: ---------------------------------- -AGENT-SPECIFIC (Process Evaluation): -- IntentResolutionEvaluator: Did the agent correctly identify user intent? -- TaskAdherenceEvaluator: Did the response follow the assigned task/system prompt? -- ToolCallAccuracyEvaluator: Were the correct tools called with proper arguments? - -QUALITY METRICS (System Evaluation): -- CoherenceEvaluator: Is the response logically coherent? -- FluencyEvaluator: Is the response well-written? -- RelevanceEvaluator: Is the response relevant to the query? -- ResponseCompletenessEvaluator: Does the response fully address the query? - -MULTI-TURN SUPPORT: -- Azure AI Foundry supports conversation format with messages list -- Each message has role (system/user/assistant/tool), content, and optional tool_calls -- Evaluators understand conversation context and tool interactions - -Reference: https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/develop/agent-evaluate-sdk -""" - -import os -import json -import asyncio -from pathlib import Path -from dataclasses import dataclass, field -from typing import Optional -from datetime import datetime - -from dotenv import load_dotenv - -# Load environment from evaluation folder -_eval_dir = Path(__file__).parent -load_dotenv(_eval_dir / ".env") - -# Azure AI Evaluation imports -try: - from azure.ai.evaluation import ( - IntentResolutionEvaluator, - TaskAdherenceEvaluator, - ToolCallAccuracyEvaluator, - CoherenceEvaluator, - FluencyEvaluator, - RelevanceEvaluator, - # ResponseCompletenessEvaluator, # May not be available in all versions - ) - EVALUATORS_AVAILABLE = True -except ImportError as e: - print(f"Warning: Azure AI Evaluation SDK not fully available: {e}") - EVALUATORS_AVAILABLE = False - - -# ═══════════════════════════════════════════════════════════════════════════════ -# DATA STRUCTURES -# ═══════════════════════════════════════════════════════════════════════════════ - - -@dataclass -class ToolCall: - """Represents a tool call made by the agent.""" - name: str - arguments: dict = field(default_factory=dict) - tool_call_id: str = "" - result: Optional[dict] = None - - -@dataclass -class ConversationMessage: - """A message in the conversation (OpenAI-style format).""" - role: str # "system", "user", "assistant", "tool" - content: str - tool_calls: list[ToolCall] = field(default_factory=list) - tool_call_id: Optional[str] = None # For tool response messages - timestamp: Optional[str] = None - - -@dataclass -class ToolDefinition: - """Definition of a tool available to the agent.""" - name: str - description: str - parameters: dict = field(default_factory=dict) - - -@dataclass -class EvaluationInput: - """Input data for LLM-judge evaluation.""" - query: str # User query or conversation history - response: str # Agent's final response - tool_calls: list[ToolCall] = field(default_factory=list) - tool_definitions: list[ToolDefinition] = field(default_factory=list) - system_prompt: str = "" # Agent's system prompt for TaskAdherence - conversation: list[ConversationMessage] = field(default_factory=list) - - -@dataclass -class EvaluationResult: - """Result from LLM-judge evaluation.""" - # Intent Resolution - intent_resolution_score: Optional[float] = None - intent_resolution_result: Optional[str] = None # "pass" or "fail" - intent_resolution_reason: Optional[str] = None - - # Task Adherence - task_adherence_score: Optional[float] = None - task_adherence_result: Optional[str] = None - task_adherence_reason: Optional[str] = None - - # Tool Call Accuracy - tool_call_accuracy_score: Optional[float] = None - tool_call_accuracy_result: Optional[str] = None - tool_call_accuracy_reason: Optional[str] = None - - # Quality Metrics - coherence_score: Optional[float] = None - fluency_score: Optional[float] = None - relevance_score: Optional[float] = None - - # Solution Accuracy (custom evaluator using ground truth + rubric) - solution_accuracy_score: Optional[float] = None - solution_accuracy_reason: Optional[str] = None - - # Overall - overall_pass: bool = False - evaluation_time: float = 0.0 - errors: list[str] = field(default_factory=list) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# MODEL CONFIGURATION -# ═══════════════════════════════════════════════════════════════════════════════ - - -def get_model_config() -> dict: - """Get Azure OpenAI model configuration for evaluators.""" - return { - "azure_endpoint": os.getenv("AZURE_OPENAI_ENDPOINT"), - "api_key": os.getenv("AZURE_OPENAI_KEY"), - "api_version": os.getenv("AZURE_OPENAI_API_VERSION", "2024-08-01-preview"), - "azure_deployment": os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-4o"), - } - - -def _safe_float(value: any) -> Optional[float]: - """Safely convert SDK output to float, handling string values.""" - if value is None: - return None - if isinstance(value, (int, float)): - return float(value) - if isinstance(value, str): - try: - return float(value) - except ValueError: - # Could be "pass" or "fail" - not a numeric score - return None - return None - - -# ═══════════════════════════════════════════════════════════════════════════════ -# SOLUTION ACCURACY EVALUATOR (Custom LLM-as-Judge) -# ═══════════════════════════════════════════════════════════════════════════════ - -SOLUTION_ACCURACY_PROMPT = """You are an expert evaluator assessing how well an AI agent's response addresses a customer service scenario. - -## Ground Truth Solution -This is the ideal/expected solution for the scenario: -{ground_truth} - -## Scoring Rubric -{scoring_rubric} - -## Agent's Response -{agent_response} - -## Your Task -Compare the agent's response against the ground truth solution using the scoring rubric. -Consider: -1. Does the response correctly identify the root cause/issue? -2. Does it provide accurate information (numbers, facts, policies)? -3. Does it offer appropriate solutions or next steps? -4. Does it address the customer's actual needs? - -Provide your evaluation in this exact format: -SCORE: [1-5] -REASON: [One paragraph explaining why you gave this score, referencing specific parts of the rubric] -""" - - -class SolutionAccuracyEvaluator: - """ - Custom evaluator that scores agent responses against ground truth solutions - using a scoring rubric. This provides domain-specific accuracy evaluation. - """ - - def __init__(self, model_config: Optional[dict] = None): - self.model_config = model_config or get_model_config() - self._client = None - - def _get_client(self): - """Lazily initialize the Azure OpenAI client.""" - if self._client is None: - try: - from openai import AzureOpenAI - self._client = AzureOpenAI( - azure_endpoint=self.model_config["azure_endpoint"], - api_key=self.model_config["api_key"], - api_version=self.model_config["api_version"], - ) - except Exception as e: - print(f"Failed to initialize OpenAI client: {e}") - return self._client - - async def evaluate( - self, - agent_response: str, - ground_truth: str, - scoring_rubric: str, - ) -> tuple[Optional[float], Optional[str]]: - """ - Evaluate agent response against ground truth using the rubric. - - Returns: - (score, reason) tuple where score is 1-5 or None on error - """ - if not ground_truth or not scoring_rubric: - return None, "No ground truth or rubric provided" - - client = self._get_client() - if not client: - return None, "OpenAI client not available" - - prompt = SOLUTION_ACCURACY_PROMPT.format( - ground_truth=ground_truth, - scoring_rubric=scoring_rubric, - agent_response=agent_response, - ) - - try: - loop = asyncio.get_event_loop() - response = await loop.run_in_executor( - None, - lambda: client.chat.completions.create( - model=self.model_config["azure_deployment"], - messages=[{"role": "user", "content": prompt}], - temperature=0.0, - max_tokens=500, - ) - ) - - content = response.choices[0].message.content - - # Parse response - score = None - reason = None - - for line in content.split("\n"): - if line.startswith("SCORE:"): - try: - score = float(line.replace("SCORE:", "").strip()) - except ValueError: - pass - elif line.startswith("REASON:"): - reason = line.replace("REASON:", "").strip() - - # If reason spans multiple lines, get the rest - if "REASON:" in content: - reason_start = content.find("REASON:") + len("REASON:") - reason = content[reason_start:].strip() - - return score, reason - - except Exception as e: - return None, f"Evaluation error: {e}" - - -# ═══════════════════════════════════════════════════════════════════════════════ -# LLM JUDGE EVALUATOR -# ═══════════════════════════════════════════════════════════════════════════════ - - -class LLMJudgeEvaluator: - """ - Evaluates agents using Azure AI Foundry's LLM-as-judge evaluators. - - This replaces simple keyword matching with sophisticated AI-assisted judgment - that can understand context, intent, and quality of responses. - - Example usage: - evaluator = LLMJudgeEvaluator() - - result = await evaluator.evaluate( - query="What's my invoice total?", - response="Your invoice total is $150.00", - tool_calls=[ToolCall(name="get_customer_invoices", arguments={"customer_id": 1})], - tool_definitions=[ToolDefinition( - name="get_customer_invoices", - description="Get invoices for a customer" - )] - ) - - print(f"Intent Resolution: {result.intent_resolution_result}") - print(f"Tool Accuracy: {result.tool_call_accuracy_result}") - """ - - def __init__( - self, - model_config: Optional[dict] = None, - use_reasoning_model: bool = False, # Set True for o-series models - enable_agent_evaluators: bool = True, - enable_quality_evaluators: bool = True, - ): - """ - Initialize LLM Judge evaluator. - - Args: - model_config: Azure OpenAI configuration (uses env vars if None) - use_reasoning_model: Set True if using o-series reasoning models - enable_agent_evaluators: Enable IntentResolution, TaskAdherence, ToolCallAccuracy - enable_quality_evaluators: Enable Coherence, Fluency, Relevance - """ - self.model_config = model_config or get_model_config() - self.use_reasoning_model = use_reasoning_model - self.enable_agent_evaluators = enable_agent_evaluators - self.enable_quality_evaluators = enable_quality_evaluators - - self._evaluators: dict = {} - self._initialized = False - - # Custom solution accuracy evaluator (always available) - self._solution_evaluator = SolutionAccuracyEvaluator(self.model_config) - - def _init_evaluators(self): - """Lazily initialize evaluators.""" - if self._initialized or not EVALUATORS_AVAILABLE: - return - - try: - if self.enable_agent_evaluators: - # Agent-specific evaluators (support reasoning models) - eval_kwargs = {"model_config": self.model_config} - if self.use_reasoning_model: - eval_kwargs["is_reasoning_model"] = True - - self._evaluators["intent_resolution"] = IntentResolutionEvaluator(**eval_kwargs) - self._evaluators["task_adherence"] = TaskAdherenceEvaluator(**eval_kwargs) - self._evaluators["tool_call_accuracy"] = ToolCallAccuracyEvaluator(**eval_kwargs) - - if self.enable_quality_evaluators: - # Quality evaluators (don't use reasoning model for efficiency) - quality_config = {"model_config": self.model_config} - self._evaluators["coherence"] = CoherenceEvaluator(**quality_config) - self._evaluators["fluency"] = FluencyEvaluator(**quality_config) - self._evaluators["relevance"] = RelevanceEvaluator(**quality_config) - - self._initialized = True - print(f"✅ Initialized {len(self._evaluators)} LLM-judge evaluators") - - except Exception as e: - print(f"⚠️ Error initializing evaluators: {e}") - self._initialized = True # Don't retry - - def _format_tool_calls(self, tool_calls: list[ToolCall]) -> list[dict]: - """Format tool calls for Azure AI Evaluation SDK.""" - return [ - { - "type": "tool_call", - "tool_call_id": tc.tool_call_id or f"call_{i}", - "name": tc.name, - "arguments": tc.arguments - } - for i, tc in enumerate(tool_calls) - ] - - def _format_tool_definitions(self, tool_definitions: list[ToolDefinition]) -> list[dict]: - """Format tool definitions for Azure AI Evaluation SDK.""" - return [ - { - "name": td.name, - "description": td.description, - "parameters": td.parameters or { - "type": "object", - "properties": {}, - } - } - for td in tool_definitions - ] - - def _format_conversation_query( - self, - query: str, - system_prompt: str, - conversation: list[ConversationMessage] - ) -> list[dict]: - """ - Format conversation history as query for multi-turn evaluation. - - Azure AI Foundry expects query as a list of OpenAI-style messages - for multi-turn evaluation. - """ - messages = [] - - # System message first (required) - if system_prompt: - messages.append({ - "role": "system", - "content": system_prompt - }) - else: - messages.append({ - "role": "system", - "content": "You are a helpful customer service agent." - }) - - # Add conversation history - for msg in conversation: - message = { - "role": msg.role, - "createdAt": msg.timestamp or datetime.now().isoformat() + "Z", - } - - if msg.role == "tool": - message["content"] = [{"type": "tool_result", "tool_result": msg.content}] - if msg.tool_call_id: - message["tool_call_id"] = msg.tool_call_id - elif msg.tool_calls: - message["content"] = [ - { - "type": "tool_call", - "tool_call_id": tc.tool_call_id or f"call_{i}", - "name": tc.name, - "arguments": tc.arguments - } - for i, tc in enumerate(msg.tool_calls) - ] - else: - message["content"] = [{"type": "text", "text": msg.content}] - - messages.append(message) - - # Final user query if not already in conversation - if not conversation or conversation[-1].role != "user": - messages.append({ - "role": "user", - "createdAt": datetime.now().isoformat() + "Z", - "content": [{"type": "text", "text": query}] - }) - - return messages - - async def evaluate( - self, - query: str, - response: str, - tool_calls: Optional[list[ToolCall]] = None, - tool_definitions: Optional[list[ToolDefinition]] = None, - system_prompt: str = "", - conversation: Optional[list[ConversationMessage]] = None, - ground_truth_solution: str = "", - scoring_rubric: str = "", - ) -> EvaluationResult: - """ - Evaluate agent response using LLM judges. - - Args: - query: The user's query - response: The agent's response - tool_calls: List of tools the agent called - tool_definitions: Available tool definitions - system_prompt: Agent's system prompt - conversation: Full conversation history for multi-turn - ground_truth_solution: The ideal/expected solution for the scenario - scoring_rubric: Criteria for evaluating solution accuracy (1-5 scale) - - Returns: - EvaluationResult with scores, pass/fail, and reasons - """ - import time - start_time = time.time() - - result = EvaluationResult() - - if not EVALUATORS_AVAILABLE: - result.errors.append("Azure AI Evaluation SDK not available") - return result - - self._init_evaluators() - - # Format inputs - formatted_tool_calls = self._format_tool_calls(tool_calls or []) - formatted_tool_defs = self._format_tool_definitions(tool_definitions or []) - - # Use conversation format for multi-turn if provided - if conversation: - formatted_query = self._format_conversation_query( - query, system_prompt, conversation - ) - else: - formatted_query = query - - # Run evaluators (they're synchronous, so we run in executor) - loop = asyncio.get_event_loop() - - # Intent Resolution - if "intent_resolution" in self._evaluators: - try: - intent_result = await loop.run_in_executor( - None, - lambda: self._evaluators["intent_resolution"]( - query=formatted_query, - response=response, - ) - ) - result.intent_resolution_score = _safe_float(intent_result.get("intent_resolution")) - result.intent_resolution_result = intent_result.get("intent_resolution_result") - result.intent_resolution_reason = intent_result.get("intent_resolution_reason") - except Exception as e: - result.errors.append(f"IntentResolution error: {e}") - - # Task Adherence - if "task_adherence" in self._evaluators: - try: - task_result = await loop.run_in_executor( - None, - lambda: self._evaluators["task_adherence"]( - query=formatted_query, - response=response, - ) - ) - result.task_adherence_score = _safe_float(task_result.get("task_adherence")) - result.task_adherence_result = task_result.get("task_adherence_result") - result.task_adherence_reason = task_result.get("task_adherence_reason") - except Exception as e: - result.errors.append(f"TaskAdherence error: {e}") - - # Tool Call Accuracy (only if tool_calls provided) - if "tool_call_accuracy" in self._evaluators and formatted_tool_calls: - try: - tool_result = await loop.run_in_executor( - None, - lambda: self._evaluators["tool_call_accuracy"]( - query=query, # Simple string for tool accuracy - tool_calls=formatted_tool_calls, - tool_definitions=formatted_tool_defs, - ) - ) - result.tool_call_accuracy_score = _safe_float(tool_result.get("tool_call_accuracy")) - result.tool_call_accuracy_result = tool_result.get("tool_call_accuracy_result") - result.tool_call_accuracy_reason = str(tool_result.get("details", "")) - except Exception as e: - result.errors.append(f"ToolCallAccuracy error: {e}") - - # Quality Metrics - if "coherence" in self._evaluators: - try: - coh_result = await loop.run_in_executor( - None, - lambda: self._evaluators["coherence"]( - query=query, - response=response, - ) - ) - result.coherence_score = _safe_float(coh_result.get("coherence")) - except Exception as e: - result.errors.append(f"Coherence error: {e}") - - if "fluency" in self._evaluators: - try: - flu_result = await loop.run_in_executor( - None, - lambda: self._evaluators["fluency"]( - query=query, - response=response, - ) - ) - result.fluency_score = _safe_float(flu_result.get("fluency")) - except Exception as e: - result.errors.append(f"Fluency error: {e}") - - if "relevance" in self._evaluators: - try: - rel_result = await loop.run_in_executor( - None, - lambda: self._evaluators["relevance"]( - query=query, - response=response, - ) - ) - result.relevance_score = _safe_float(rel_result.get("relevance")) - except Exception as e: - result.errors.append(f"Relevance error: {e}") - - # Solution Accuracy (custom evaluator with ground truth + rubric) - if ground_truth_solution and scoring_rubric: - try: - score, reason = await self._solution_evaluator.evaluate( - agent_response=response, - ground_truth=ground_truth_solution, - scoring_rubric=scoring_rubric, - ) - result.solution_accuracy_score = score - result.solution_accuracy_reason = reason - except Exception as e: - result.errors.append(f"SolutionAccuracy error: {e}") - - # Determine overall pass - passes = [] - if result.intent_resolution_result: - passes.append(result.intent_resolution_result == "pass") - if result.task_adherence_result: - passes.append(result.task_adherence_result == "pass") - if result.tool_call_accuracy_result: - passes.append(result.tool_call_accuracy_result == "pass") - # Solution accuracy: pass if score >= 3 (Adequate or better) - if result.solution_accuracy_score is not None: - passes.append(result.solution_accuracy_score >= 3) - - result.overall_pass = all(passes) if passes else False - result.evaluation_time = time.time() - start_time - - return result - - def evaluate_sync(self, **kwargs) -> EvaluationResult: - """Synchronous wrapper for evaluate().""" - return asyncio.run(self.evaluate(**kwargs)) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# CONVENIENCE FUNCTIONS -# ═══════════════════════════════════════════════════════════════════════════════ - - -async def evaluate_agent_response( - query: str, - response: str, - tool_calls: Optional[list[str]] = None, # Just tool names for simplicity - tool_definitions: Optional[list[dict]] = None, -) -> EvaluationResult: - """ - Simple function to evaluate an agent response. - - Args: - query: User's query - response: Agent's response - tool_calls: List of tool names that were called - tool_definitions: List of {name, description} dicts - - Returns: - EvaluationResult with scores and pass/fail - """ - evaluator = LLMJudgeEvaluator() - - # Convert simple tool names to ToolCall objects - tc_objects = [ToolCall(name=name) for name in (tool_calls or [])] - - # Convert simple dicts to ToolDefinition objects - td_objects = [ - ToolDefinition( - name=td.get("name", ""), - description=td.get("description", "") - ) - for td in (tool_definitions or []) - ] - - return await evaluator.evaluate( - query=query, - response=response, - tool_calls=tc_objects, - tool_definitions=td_objects, - ) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# DEMO -# ═══════════════════════════════════════════════════════════════════════════════ - - -async def demo(): - """Demo the LLM judge evaluator.""" - print("=" * 80) - print("LLM-as-Judge Evaluator Demo") - print("=" * 80) - - if not EVALUATORS_AVAILABLE: - print("\n❌ Azure AI Evaluation SDK not available.") - print("Install with: pip install azure-ai-evaluation") - return - - # Check required environment variables - required_vars = ["AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_KEY"] - missing = [v for v in required_vars if not os.getenv(v)] - if missing: - print(f"\n⚠️ Missing environment variables: {missing}") - print("Set these in tests/evaluation/.env") - return - - evaluator = LLMJudgeEvaluator( - enable_agent_evaluators=True, - enable_quality_evaluators=True, - ) - - # Test case: Customer asks about invoice - print("\n📋 Test Case: Invoice Query") - print("-" * 40) - - result = await evaluator.evaluate( - query="What is my current invoice total for account 123?", - response="Based on your account records, your current invoice total is $542.50. This includes your monthly subscription fee of $99.99, data overage charges of $42.51, and equipment rental of $400.00.", - tool_calls=[ - ToolCall( - name="get_customer_invoices", - arguments={"customer_id": 123} - ) - ], - tool_definitions=[ - ToolDefinition( - name="get_customer_invoices", - description="Retrieves invoice details for a customer account" - ), - ToolDefinition( - name="get_customer_detail", - description="Gets customer profile information" - ), - ], - ) - - print(f"\n🎯 Intent Resolution:") - print(f" Score: {result.intent_resolution_score}/5") - print(f" Result: {result.intent_resolution_result}") - print(f" Reason: {result.intent_resolution_reason}") - - print(f"\n📋 Task Adherence:") - print(f" Score: {result.task_adherence_score}/5") - print(f" Result: {result.task_adherence_result}") - print(f" Reason: {result.task_adherence_reason}") - - print(f"\n🔧 Tool Call Accuracy:") - print(f" Score: {result.tool_call_accuracy_score}/5") - print(f" Result: {result.tool_call_accuracy_result}") - - print(f"\n✨ Quality Metrics:") - print(f" Coherence: {result.coherence_score}/5") - print(f" Fluency: {result.fluency_score}/5") - print(f" Relevance: {result.relevance_score}/5") - - print(f"\n{'✅ OVERALL PASS' if result.overall_pass else '❌ OVERALL FAIL'}") - print(f"⏱️ Evaluation time: {result.evaluation_time:.2f}s") - - if result.errors: - print(f"\n⚠️ Errors: {result.errors}") - - -if __name__ == "__main__": - asyncio.run(demo()) diff --git a/tests/evaluation/pyproject.toml b/tests/evaluation/pyproject.toml deleted file mode 100644 index fea5a82c0..000000000 --- a/tests/evaluation/pyproject.toml +++ /dev/null @@ -1,42 +0,0 @@ -[project] -name = "agent-evaluation" -version = "0.1.0" -description = "Agent evaluation framework for Contoso AI Workshop" -readme = "README.md" -requires-python = ">=3.12" -dependencies = [ - # Agent Framework (same as applications) - "agent-framework==1.0.0b260107", - - # Azure OpenAI - "openai>=2.5.0", - "azure-identity>=1.15.0", - - # Testing - "pytest>=8.0.0", - "pytest-asyncio>=0.23.0", - "pytest-timeout>=2.3.0", - - # Evaluation SDK (optional - for AI-assisted metrics) - "azure-ai-evaluation>=1.0.0", - - # Utilities - "python-dotenv>=1.0.0", - "httpx>=0.27.0", - "pydantic>=2.0.0", -] - -[tool.uv] -prerelease = "allow" - -[tool.pytest.ini_options] -asyncio_mode = "auto" -testpaths = ["."] -python_files = ["test_*.py", "*_test.py"] -markers = [ - "unit: Unit tests (no external dependencies)", - "integration: Integration tests (require MCP + Azure OpenAI)", - "evaluation: Agent evaluation tests", - "slow: Slow tests (full evaluation pipeline)", - "comparison: Agent comparison tests", -] diff --git a/tests/evaluation/test_agent_comparison.py b/tests/evaluation/test_agent_comparison.py deleted file mode 100644 index f9deb4dae..000000000 --- a/tests/evaluation/test_agent_comparison.py +++ /dev/null @@ -1,484 +0,0 @@ -""" -Agent Comparison Tests - -This module compares the performance of different agent implementations -(single_agent vs reflection_agent) using the same test dataset. - -Usage: - # Run from tests/evaluation folder: - uv run pytest test_agent_comparison.py -v - - # Run with detailed comparison report: - uv run pytest test_agent_comparison.py -v -s --tb=short - - # Run quick comparison (fewer test cases): - uv run pytest test_agent_comparison.py -v -k "quick" -""" - -import asyncio -import json -import os -import sys -import time -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any - -import pytest -from dotenv import load_dotenv - -# ═══════════════════════════════════════════════════════════════════════════════ -# PATH SETUP -# ═══════════════════════════════════════════════════════════════════════════════ - -# Add paths for imports -_eval_dir = Path(__file__).parent.resolve() -_tests_dir = _eval_dir.parent -_workspace_root = _tests_dir.parent -_agentic_ai_dir = _workspace_root / "agentic_ai" - -# Add paths for agent imports -sys.path.insert(0, str(_agentic_ai_dir)) -sys.path.insert(0, str(_tests_dir)) - -# Load environment from evaluation folder -load_dotenv(_eval_dir / ".env") - -# ═══════════════════════════════════════════════════════════════════════════════ -# DATA CLASSES -# ═══════════════════════════════════════════════════════════════════════════════ - - -@dataclass -class AgentResult: - """Result from a single agent run.""" - - agent_name: str - query: str - response: str - tools_called: list[str] - execution_time: float - error: str | None = None - - @property - def success(self) -> bool: - return self.error is None and bool(self.response) - - -@dataclass -class ComparisonMetrics: - """Comparison metrics between two agents.""" - - agent_a: str - agent_b: str - test_count: int - agent_a_metrics: dict[str, float] = field(default_factory=dict) - agent_b_metrics: dict[str, float] = field(default_factory=dict) - differences: dict[str, float] = field(default_factory=dict) - - def to_report(self) -> str: - """Generate a formatted comparison report.""" - lines = [ - "═" * 70, - f"AGENT COMPARISON REPORT: {self.agent_a} vs {self.agent_b}", - "═" * 70, - f"Test Cases: {self.test_count}", - "", - f"{'Metric':<30} {self.agent_a:>15} {self.agent_b:>15} {'Diff':>10}", - "-" * 70, - ] - - all_metrics = set(self.agent_a_metrics.keys()) | set(self.agent_b_metrics.keys()) - for metric in sorted(all_metrics): - a_val = self.agent_a_metrics.get(metric, 0) - b_val = self.agent_b_metrics.get(metric, 0) - diff = self.differences.get(metric, b_val - a_val) - diff_str = f"+{diff:.3f}" if diff > 0 else f"{diff:.3f}" - lines.append(f"{metric:<30} {a_val:>15.3f} {b_val:>15.3f} {diff_str:>10}") - - lines.extend(["-" * 70, ""]) - return "\n".join(lines) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# AGENT RUNNER -# ═══════════════════════════════════════════════════════════════════════════════ - - -class AgentComparisonRunner: - """Runs and compares multiple agent implementations.""" - - def __init__(self): - self.mcp_server_uri = os.getenv("MCP_SERVER_URI", "http://localhost:8000/mcp") - self.azure_endpoint = os.getenv("AZURE_OPENAI_ENDPOINT") - self.azure_api_key = os.getenv("AZURE_OPENAI_API_KEY") - self.deployment = os.getenv("AZURE_OPENAI_CHAT_DEPLOYMENT") - self.api_version = os.getenv("AZURE_OPENAI_API_VERSION", "2025-03-01-preview") - - # Agent module paths - self.single_agent_module = os.getenv( - "SINGLE_AGENT_MODULE", - "agents.agent_framework.single_agent" - ) - self.reflection_agent_module = os.getenv( - "REFLECTION_AGENT_MODULE", - "agents.agent_framework.multi_agent.reflection_agent" - ) - - async def _run_single_agent(self, query: str, session_id: str | None = None) -> AgentResult: - """Run the single agent implementation.""" - from agents.agent_framework.single_agent import Agent - - start_time = time.time() - tools_called = [] - error = None - response = "" - - # Use unique session ID for isolation - if session_id is None: - session_id = f"eval_single_{int(time.time() * 1000)}" - - state_store: dict[str, Any] = {} - - try: - # Create agent instance - agent = Agent(state_store=state_store, session_id=session_id) - - # Run the agent - response = await agent.chat_async(query) - - # Try to extract tool calls from chat history if available - if hasattr(agent, 'chat_history'): - for entry in agent.chat_history: - if isinstance(entry, dict) and 'tool_calls' in entry: - for tc in entry.get('tool_calls', []): - if isinstance(tc, dict): - tools_called.append(tc.get('name', str(tc))) - else: - tools_called.append(str(tc)) - - except Exception as e: - error = str(e) - response = "" - - return AgentResult( - agent_name="single_agent", - query=query, - response=response, - tools_called=tools_called, - execution_time=time.time() - start_time, - error=error - ) - - async def _run_reflection_agent(self, query: str, session_id: str | None = None) -> AgentResult: - """Run the reflection agent implementation.""" - from agents.agent_framework.multi_agent.reflection_agent import Agent - - start_time = time.time() - tools_called = [] - error = None - response = "" - - # Use unique session ID for isolation - if session_id is None: - session_id = f"eval_reflection_{int(time.time() * 1000)}" - - state_store: dict[str, Any] = {} - - try: - # Create agent instance - agent = Agent(state_store=state_store, session_id=session_id) - - # Run the agent - response = await agent.chat_async(query) - - # Try to extract tool calls from chat history if available - if hasattr(agent, 'chat_history'): - for entry in agent.chat_history: - if isinstance(entry, dict) and 'tool_calls' in entry: - for tc in entry.get('tool_calls', []): - if isinstance(tc, dict): - tools_called.append(tc.get('name', str(tc))) - else: - tools_called.append(str(tc)) - - except Exception as e: - error = str(e) - response = "" - - return AgentResult( - agent_name="reflection_agent", - query=query, - response=response, - tools_called=tools_called, - execution_time=time.time() - start_time, - error=error - ) - - async def run_comparison( - self, - queries: list[str], - expected_tools: list[list[str]] | None = None - ) -> ComparisonMetrics: - """Run both agents on a list of queries and compare results.""" - - single_results: list[AgentResult] = [] - reflection_results: list[AgentResult] = [] - - for i, query in enumerate(queries): - print(f"\n[{i+1}/{len(queries)}] Testing: {query[:50]}...") - - # Run both agents - single_result = await self._run_single_agent(query) - single_results.append(single_result) - print(f" Single Agent: {single_result.execution_time:.2f}s, " - f"tools={len(single_result.tools_called)}, " - f"success={single_result.success}") - - reflection_result = await self._run_reflection_agent(query) - reflection_results.append(reflection_result) - print(f" Reflection Agent: {reflection_result.execution_time:.2f}s, " - f"tools={len(reflection_result.tools_called)}, " - f"success={reflection_result.success}") - - # Calculate metrics - metrics = ComparisonMetrics( - agent_a="single_agent", - agent_b="reflection_agent", - test_count=len(queries) - ) - - # Calculate single agent metrics - metrics.agent_a_metrics = self._calculate_metrics(single_results, expected_tools) - - # Calculate reflection agent metrics - metrics.agent_b_metrics = self._calculate_metrics(reflection_results, expected_tools) - - # Calculate differences (reflection - single) - for key in metrics.agent_a_metrics: - metrics.differences[key] = ( - metrics.agent_b_metrics.get(key, 0) - - metrics.agent_a_metrics.get(key, 0) - ) - - return metrics - - def _calculate_metrics( - self, - results: list[AgentResult], - expected_tools: list[list[str]] | None = None - ) -> dict[str, float]: - """Calculate aggregate metrics from results.""" - if not results: - return {} - - metrics = {} - - # Success rate - success_count = sum(1 for r in results if r.success) - metrics["success_rate"] = success_count / len(results) - - # Average execution time - metrics["avg_execution_time"] = sum(r.execution_time for r in results) / len(results) - - # Average response length - metrics["avg_response_length"] = sum(len(r.response) for r in results) / len(results) - - # Average tools called - metrics["avg_tools_called"] = sum(len(r.tools_called) for r in results) / len(results) - - # Tool accuracy (if expected tools provided) - if expected_tools and len(expected_tools) == len(results): - tool_accuracies = [] - for result, expected in zip(results, expected_tools): - if not expected: - continue - called_set = set(result.tools_called) - expected_set = set(expected) - if expected_set: - accuracy = len(called_set & expected_set) / len(expected_set) - tool_accuracies.append(accuracy) - - if tool_accuracies: - metrics["tool_accuracy"] = sum(tool_accuracies) / len(tool_accuracies) - - return metrics - - -# ═══════════════════════════════════════════════════════════════════════════════ -# TEST DATA LOADING -# ═══════════════════════════════════════════════════════════════════════════════ - - -def load_test_data(count: int | None = None) -> tuple[list[str], list[list[str]], list[str]]: - """Load test data from test_data.jsonl. - - Returns: - Tuple of (queries, expected_tools, ground_truths) - """ - test_data_file = _eval_dir / "test_data.jsonl" - queries = [] - expected_tools = [] - ground_truths = [] - - with open(test_data_file, "r") as f: - for line in f: - if not line.strip(): - continue - data = json.loads(line) - queries.append(data["query"]) - expected_tools.append(data.get("expected_tools", [])) - ground_truths.append(data.get("ground_truth", "")) - - if count and len(queries) >= count: - break - - return queries, expected_tools, ground_truths - - -# ═══════════════════════════════════════════════════════════════════════════════ -# TESTS -# ═══════════════════════════════════════════════════════════════════════════════ - - -@pytest.fixture -def comparison_runner(): - """Create an agent comparison runner.""" - return AgentComparisonRunner() - - -class TestAgentComparison: - """Tests that compare single_agent vs reflection_agent.""" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_quick_comparison(self, comparison_runner): - """Quick comparison using 3 test cases.""" - quick_count = int(os.getenv("EVAL_QUICK_TEST_COUNT", "3")) - queries, expected_tools, _ = load_test_data(count=quick_count) - - metrics = await comparison_runner.run_comparison(queries, expected_tools) - - print("\n" + metrics.to_report()) - - # Both agents should have some level of success - assert metrics.agent_a_metrics.get("success_rate", 0) >= 0.5, \ - "Single agent success rate too low" - assert metrics.agent_b_metrics.get("success_rate", 0) >= 0.5, \ - "Reflection agent success rate too low" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_full_comparison(self, comparison_runner): - """Full comparison using all test cases.""" - queries, expected_tools, _ = load_test_data() - - metrics = await comparison_runner.run_comparison(queries, expected_tools) - - print("\n" + metrics.to_report()) - - # Store results for analysis - results_file = _eval_dir / "comparison_results.json" - with open(results_file, "w") as f: - json.dump({ - "agent_a": metrics.agent_a, - "agent_b": metrics.agent_b, - "test_count": metrics.test_count, - "agent_a_metrics": metrics.agent_a_metrics, - "agent_b_metrics": metrics.agent_b_metrics, - "differences": metrics.differences - }, f, indent=2) - - print(f"\nResults saved to: {results_file}") - - # Basic assertions - assert metrics.test_count > 0, "No tests were run" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_execution_time_comparison(self, comparison_runner): - """Compare execution times between agents.""" - queries, expected_tools, _ = load_test_data(count=3) - - metrics = await comparison_runner.run_comparison(queries, expected_tools) - - single_time = metrics.agent_a_metrics.get("avg_execution_time", 0) - reflection_time = metrics.agent_b_metrics.get("avg_execution_time", 0) - - print(f"\nExecution Time Comparison:") - print(f" Single Agent: {single_time:.2f}s") - print(f" Reflection Agent: {reflection_time:.2f}s") - print(f" Difference: {reflection_time - single_time:.2f}s") - - # Reflection agent is expected to take longer (more LLM calls) - # Just verify times are reasonable - assert single_time > 0, "Single agent should have non-zero execution time" - assert reflection_time > 0, "Reflection agent should have non-zero execution time" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_response_quality_comparison(self, comparison_runner): - """Compare response quality metrics between agents.""" - queries, expected_tools, _ = load_test_data(count=3) - - metrics = await comparison_runner.run_comparison(queries, expected_tools) - - print("\nResponse Quality Comparison:") - print(f" Single Agent Response Length: {metrics.agent_a_metrics.get('avg_response_length', 0):.0f}") - print(f" Reflection Agent Response Length: {metrics.agent_b_metrics.get('avg_response_length', 0):.0f}") - - # Both should produce responses - assert metrics.agent_a_metrics.get("avg_response_length", 0) > 0 - assert metrics.agent_b_metrics.get("avg_response_length", 0) > 0 - - -class TestSingleAgentOnly: - """Tests for single agent in isolation.""" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_single_agent_basic(self, comparison_runner): - """Test single agent with a basic query.""" - result = await comparison_runner._run_single_agent( - "Hello, I need help with my account" - ) - - assert result.success, f"Single agent failed: {result.error}" - assert len(result.response) > 0, "Response should not be empty" - print(f"\nSingle Agent Response: {result.response[:200]}...") - - -class TestReflectionAgentOnly: - """Tests for reflection agent in isolation.""" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_reflection_agent_basic(self, comparison_runner): - """Test reflection agent with a basic query.""" - result = await comparison_runner._run_reflection_agent( - "Hello, I need help with my account" - ) - - assert result.success, f"Reflection agent failed: {result.error}" - assert len(result.response) > 0, "Response should not be empty" - print(f"\nReflection Agent Response: {result.response[:200]}...") - - -# ═══════════════════════════════════════════════════════════════════════════════ -# STANDALONE EXECUTION -# ═══════════════════════════════════════════════════════════════════════════════ - -if __name__ == "__main__": - """Run comparison directly without pytest.""" - async def main(): - runner = AgentComparisonRunner() - queries, expected_tools, _ = load_test_data(count=3) - - print("Running Agent Comparison...") - print(f"Comparing: single_agent vs reflection_agent") - print(f"Test cases: {len(queries)}") - - metrics = await runner.run_comparison(queries, expected_tools) - print(metrics.to_report()) - - asyncio.run(main()) diff --git a/tests/evaluation/test_data.jsonl b/tests/evaluation/test_data.jsonl deleted file mode 100644 index 3bc2f1cfa..000000000 --- a/tests/evaluation/test_data.jsonl +++ /dev/null @@ -1,10 +0,0 @@ -{"query": "I noticed my last invoice was higher than usual—can you help me understand why and what can be done about it?", "customer_id": "251", "expected_intent": "billing_inquiry", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_billing_summary", "search_knowledge_base"], "ground_truth": "Customer 251 (John Doe) has invoice showing $150 which is 2.5x usual. Agent should detect data overage (22GB vs 10GB cap), quote Data Overage Policy about retroactive upgrade within 15 days, and offer invoice adjustment or plan upgrade with pro-rata credit.", "category": "billing", "complexity": "medium"} -{"query": "My internet service seems slower than before—can you check what's happening?", "customer_id": "252", "expected_intent": "service_issue", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_data_usage", "search_knowledge_base"], "ground_truth": "Customer 252 (Jane Doe, Gold loyalty) has 1Gbps plan but service_status is 'slow'. Agent should check ServiceIncidents for open ticket, reference KB Troubleshooting Slow Internet, suggest speed test and reboot, escalate if below 25% of tier.", "category": "technical_support", "complexity": "medium"} -{"query": "I'm traveling abroad next month. What should I do about my phone plan?", "customer_id": "253", "expected_intent": "plan_inquiry", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_products", "search_knowledge_base"], "ground_truth": "Customer 253 (Mark Doe, Bronze) has roaming_enabled=0. Agent should verify roaming not active, suggest International Roaming add-on, quote KB about activating 3+ days ahead, offer immediate activation with pro-rated charges.", "category": "products", "complexity": "medium"} -{"query": "I tried logging into my account, but it says I'm locked out. Can you help?", "customer_id": "254", "expected_intent": "security_issue", "expected_tools": ["get_customer_detail", "get_security_logs", "unlock_account", "search_knowledge_base"], "ground_truth": "Customer 254 (Alice Doe, Gold) has account_locked event in SecurityLogs from 12 min ago. Agent should follow Account Unlock Procedure: send 2FA code, verify identity, force password reset.", "category": "security", "complexity": "high"} -{"query": "Do I qualify for any discounts or promotions right now?", "customer_id": "255", "expected_intent": "promotion_inquiry", "expected_tools": ["get_customer_detail", "get_eligible_promotions", "get_promotions"], "ground_truth": "Customer 255 (Ron Doe, Gold loyalty) should qualify for promotions matching loyalty_level='Gold' with current dates. Agent should return Mobile Loyalty Discount (10%) and explain any future promos not yet active per Promotion Eligibility Guidelines.", "category": "promotions", "complexity": "low"} -{"query": "I want to return a product I recently purchased. What's the process?", "customer_id": "256", "expected_intent": "return_request", "expected_tools": ["get_customer_orders", "search_knowledge_base"], "ground_truth": "Customer 256 (Mary Doe, Silver) has Orders.order_status='returned' from 25 days ago within 30-day window. Agent should cite Return Policy and Process: 7-10 business days for refund, escalate if over 10 days passed.", "category": "orders", "complexity": "low"} -{"query": "Customer 251, what's my billing summary?", "customer_id": "251", "expected_intent": "billing_inquiry", "expected_tools": ["get_billing_summary", "get_customer_detail"], "ground_truth": "Customer 251 (John Doe) has $100 balance remaining ($50 already paid of $150 invoice). Agent should retrieve and present current outstanding balance across all subscriptions.", "category": "billing", "complexity": "low"} -{"query": "I keep getting dropped calls whenever I'm downtown. Can you help fix this?", "customer_id": "257", "expected_intent": "support_request", "expected_tools": ["get_support_tickets", "get_customer_detail", "create_support_ticket", "search_knowledge_base"], "ground_truth": "Customer 257 (Tom Smith, Silver) has SupportTickets category 'call_drop'. Agent should follow KB Dropped Call Investigation Workflow, capture times/locations, escalate to RF engineering, apply credit per Service Reliability SLA Credit Matrix if systemic.", "category": "support", "complexity": "medium"} -{"query": "My service is suspended because I missed a payment. Can you help restore it and waive the late fee?", "customer_id": "258", "expected_intent": "payment_issue", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_invoice_payments", "search_knowledge_base"], "ground_truth": "Customer 258 (Sara Lee, Gold) has invoice unpaid >14 days, 2 failed payment attempts, account status='inactive'. Agent should reference Payment Failure & Reinstatement Rules and Late Payment Fee Policy, offer first-time waiver eligibility and hardship plan per Financial Hardship Payment Plan Procedure.", "category": "billing", "complexity": "high"} -{"query": "I received a $150 bill due to data overage. Can you explain and help reduce it?", "customer_id": "259", "expected_intent": "data_overage", "expected_tools": ["get_customer_detail", "get_subscription_detail", "get_data_usage", "search_knowledge_base"], "ground_truth": "Customer 259 (Alex Brown, Bronze) used ~22GB vs 10GB cap = 12GB over. Agent should quote Data Overage Policy: can switch to higher tier retroactively within 15 days of invoice, overage will be re-rated. Upsell larger plan or unlimited bundle.", "category": "billing", "complexity": "high"} diff --git a/tests/evaluation/test_scenario_evaluation.py b/tests/evaluation/test_scenario_evaluation.py deleted file mode 100644 index 08cd35262..000000000 --- a/tests/evaluation/test_scenario_evaluation.py +++ /dev/null @@ -1,2020 +0,0 @@ -""" -Scenario-Based Agent Evaluation - -This module provides comprehensive evaluation for agents: -1. Goal-Based (Outcome): Did the user get what they needed? (LLM-as-Judge or keyword matching) -2. Process-Based (Tool Accuracy): Did the agent use the right tools? (LLM-as-Judge or F1 score) - -Uses the AgentTestRunner for a consistent interface across all agents. -Optionally uses Azure AI Foundry LLM-as-Judge evaluators for more sophisticated evaluation. - -Usage: - cd tests/evaluation - uv run pytest test_scenario_evaluation.py -v -s - - # With LLM-as-Judge (set EVAL_USE_LLM_JUDGE=true in .env) - uv run pytest test_scenario_evaluation.py::TestAgentComparison -v -s -""" - -import asyncio -import json -import os -import sys -import time -from dataclasses import dataclass, field -from pathlib import Path -from typing import Any, Optional - -import pytest -from dotenv import load_dotenv - -# ═══════════════════════════════════════════════════════════════════════════════ -# PATH SETUP -# ═══════════════════════════════════════════════════════════════════════════════ - -_eval_dir = Path(__file__).parent.resolve() -_tests_dir = _eval_dir.parent -_workspace_root = _tests_dir.parent -_agentic_ai_dir = _workspace_root / "agentic_ai" - -sys.path.insert(0, str(_agentic_ai_dir)) -sys.path.insert(0, str(_tests_dir)) -sys.path.insert(0, str(_eval_dir)) - -load_dotenv(_eval_dir / ".env") - -# Import the generic agent runner (ToolCallTracker is bundled in the runner) -from agent_runner import AgentTestRunner, QueryResult - -# Import LLM judge evaluator -try: - from llm_judge_evaluator import ( - LLMJudgeEvaluator, - ToolCall, - ToolDefinition, - EvaluationResult, - EVALUATORS_AVAILABLE, - ) - LLM_JUDGE_AVAILABLE = EVALUATORS_AVAILABLE -except ImportError: - LLM_JUDGE_AVAILABLE = False - -# Check if LLM judge should be used -USE_LLM_JUDGE = os.getenv("EVAL_USE_LLM_JUDGE", "false").lower() in ("true", "1", "yes") - - -# ═══════════════════════════════════════════════════════════════════════════════ -# MCP TOOL DEFINITIONS (for LLM judge) -# ═══════════════════════════════════════════════════════════════════════════════ - -MCP_TOOL_DEFINITIONS = [ - {"name": "get_customer_detail", "description": "Get customer profile and account information"}, - {"name": "get_billing_summary", "description": "Get billing and invoice summary for a customer"}, - {"name": "get_subscription_detail", "description": "Get subscription plan details including data caps and features"}, - {"name": "get_data_usage", "description": "Get current data usage statistics"}, - {"name": "get_security_logs", "description": "Get security audit logs for account access attempts"}, - {"name": "unlock_account", "description": "Unlock a customer account after verification"}, - {"name": "get_products", "description": "List available products and add-ons"}, - {"name": "get_support_tickets", "description": "Get support ticket history"}, - {"name": "search_knowledge", "description": "Search the knowledge base for policies and procedures"}, -] - - -# ═══════════════════════════════════════════════════════════════════════════════ -# SCENARIO DEFINITIONS -# ═══════════════════════════════════════════════════════════════════════════════ - - -@dataclass -class ConversationTurn: - """A single turn in the conversation.""" - user_message: str - expected_tool_calls: list[str] = field(default_factory=list) - expected_keywords: list[str] = field(default_factory=list) # Keywords in response - - -@dataclass -class Scenario: - """A complete customer scenario for evaluation.""" - id: str - name: str - description: str - customer_id: int - - # Conversation flow - turns: list[ConversationTurn] = field(default_factory=list) - - # Expected tools across entire conversation - expected_tools: list[str] = field(default_factory=list) - - # Expected outcome keywords (should appear in final response) - success_keywords: list[str] = field(default_factory=list) - - # Expected resolution (for AI evaluation) - expected_resolution: str = "" - - # Ground truth solution - the ideal/correct solution - ground_truth_solution: str = "" - - # Scoring rubric - criteria for evaluating solution accuracy - scoring_rubric: str = "" - - -# Define scenarios based on customer_scenarios.md and data_seeding.py -# MCP tool names: get_customer_detail, get_subscription_detail, get_data_usage, -# get_billing_summary, get_security_logs, unlock_account, get_products, -# search_knowledge, get_support_tickets, etc. -# -# Customer ID ranges: -# - 251-254: Documented scenarios from customer_scenarios.md -# - 1-50: Randomly generated customers in data_seeding.py (use for new scenarios) -SCENARIOS = [ - # ═══════════════════════════════════════════════════════════════════════════════ - # BILLING & PAYMENT SCENARIOS (5 scenarios) - # ═══════════════════════════════════════════════════════════════════════════════ - Scenario( - id="billing_high_invoice", - name="Invoice Higher Than Usual", - description="Customer 251 has invoice $150, 2.5x the usual amount", - customer_id=251, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 251. I noticed my last invoice was $150, which is much higher than usual. Can you help me understand why?", - expected_tool_calls=["get_billing_summary", "get_data_usage"], - expected_keywords=["invoice", "overage", "data", "usage"], - ), - ], - expected_tools=[ - "get_customer_detail", - "get_billing_summary", - "get_subscription_detail", - "get_data_usage", - "search_knowledge", - ], - success_keywords=["overage", "data", "upgrade", "adjustment", "22", "10"], - expected_resolution="Identify data overage (22GB vs 10GB cap), quote Data Overage Policy, offer adjustment or plan upgrade", - ground_truth_solution="""The customer's invoice is $150 instead of the usual $60 because of data overage charges. -Key facts to communicate: -1. The customer's plan has a 10GB data cap -2. The customer used 22GB this billing cycle (12GB over the limit) -3. Overage charges of $7.50/GB apply per the Data Overage Policy -4. The additional $90 in charges (12GB x $7.50) explains the higher bill - -Recommended solutions: -- Offer a one-time courtesy adjustment (if first offense) -- Recommend upgrading to a higher data plan or unlimited plan -- Set up data usage alerts to prevent future overages""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Correctly identifies overage (22GB vs 10GB), explains charges clearly, offers both adjustment AND upgrade options -4 - Good: Identifies overage and explains charges, offers at least one solution option -3 - Adequate: Identifies overage as the cause but missing specific numbers or only partial solution -2 - Poor: Vague explanation, doesn't clearly identify the cause or missing key details -1 - Fail: Incorrect explanation or completely unhelpful response""", - ), - - Scenario( - id="billing_payment_history", - name="Payment History Inquiry", - description="Customer wants to see recent payment history and payment methods", - customer_id=5, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 5. Can you show me my recent payments? I want to make sure they all went through.", - expected_tool_calls=["get_billing_summary"], - expected_keywords=["payment", "successful", "history"], - ), - ], - expected_tools=["get_customer_detail", "get_billing_summary"], - success_keywords=["payment", "successful", "credit_card", "amount", "date"], - expected_resolution="Retrieve payment history and confirm successful payments", - ground_truth_solution="""Show the customer their recent payment history. -Key information to provide: -1. List recent payments with dates and amounts -2. Confirm payment methods used (credit card, bank transfer, etc.) -3. Identify any failed or pending payments -4. Provide current account balance if any - -Helpful actions: -- Confirm all payments were successful -- Mention autopay option if not enabled -- Offer to send payment receipt copies if needed""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows payment history with dates, amounts, methods, and confirms all went through -4 - Good: Shows payment history and confirms status -3 - Adequate: Provides payment information but incomplete details -2 - Poor: Vague response without specific payment details -1 - Fail: Doesn't provide payment information""", - ), - - Scenario( - id="billing_autopay_setup", - name="Autopay Setup Request", - description="Customer wants to enable automatic payments", - customer_id=10, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 10. I keep forgetting to pay my bill on time. Can you help me set up autopay?", - expected_tool_calls=["get_billing_summary", "search_knowledge"], - expected_keywords=["autopay", "automatic", "payment"], - ), - ], - expected_tools=["get_customer_detail", "get_billing_summary", "get_subscription_detail", "search_knowledge"], - success_keywords=["autopay", "automatic", "$5", "discount", "enable"], - expected_resolution="Check current autopay status, explain autopay benefits including $5 discount, guide through setup", - ground_truth_solution="""Help customer set up automatic payments. -Key information to provide: -1. Current autopay status (enabled or disabled) -2. Autopay includes a $5 monthly discount -3. Explain how autopay works (auto-charge on due date) - -Required actions: -- Check current billing/subscription status -- Explain the $5 autopay discount -- Guide through the setup process -- Confirm payment method on file""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Checks status, mentions $5 discount, explains benefits, and guides through setup -4 - Good: Explains autopay benefits and how to set it up -3 - Adequate: Provides basic autopay information -2 - Poor: Generic response without checking account -1 - Fail: Doesn't help with autopay setup""", - ), - - Scenario( - id="billing_overdue_invoice", - name="Overdue Invoice Question", - description="Customer has overdue invoices and wants to understand implications", - customer_id=15, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 15. I received a notice about an overdue invoice. What happens if I don't pay soon?", - expected_tool_calls=["get_billing_summary"], - expected_keywords=["overdue", "payment", "service"], - ), - ], - expected_tools=["get_customer_detail", "get_billing_summary", "search_knowledge"], - success_keywords=["overdue", "payment", "suspension", "late", "fee"], - expected_resolution="Show overdue invoices, explain late payment consequences, offer payment options", - ground_truth_solution="""Address overdue invoice concerns. -Key information to provide: -1. List overdue invoices with amounts and due dates -2. Explain late fee policy (if applicable) -3. Potential service suspension after 30+ days overdue -4. Payment options available - -Recommended actions: -- Show specific overdue amount -- Explain consequences (late fees, service suspension) -- Offer payment plan if large amount -- Process payment immediately if customer wants""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows overdue details, explains consequences, and offers solutions/payment options -4 - Good: Explains consequences and helps with payment -3 - Adequate: Addresses concern but missing specifics -2 - Poor: Generic response without checking account -1 - Fail: Doesn't address the overdue concern""", - ), - - Scenario( - id="billing_refund_request", - name="Refund Request for Service Issue", - description="Customer wants refund for days without service", - customer_id=20, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 20. I was without internet for 3 days last week. Can I get a refund or credit for those days?", - expected_tool_calls=["get_support_tickets", "get_billing_summary"], - expected_keywords=["credit", "refund", "outage", "service"], - ), - ], - expected_tools=["get_customer_detail", "get_support_tickets", "get_subscription_detail", "get_billing_summary"], - success_keywords=["credit", "refund", "outage", "days", "pro-rated"], - expected_resolution="Verify outage via tickets/incidents, calculate pro-rated credit, apply to account", - ground_truth_solution="""Process refund request for service outage. -Key information to verify: -1. Check support tickets for reported outage -2. Verify service incident records -3. Calculate pro-rated credit (3 days of monthly fee) - -Recommended actions: -- Verify the outage occurred (via tickets or incidents) -- Calculate appropriate credit amount -- Apply credit to next invoice -- Apologize for the inconvenience -- Confirm credit will appear on next bill""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Verifies outage, calculates pro-rated credit, applies credit, and confirms -4 - Good: Acknowledges issue and offers appropriate credit -3 - Adequate: Offers to help with credit but missing verification -2 - Poor: Generic response without checking history -1 - Fail: Doesn't address the refund request""", - ), - - # ═══════════════════════════════════════════════════════════════════════════════ - # INTERNET & CONNECTIVITY SCENARIOS (5 scenarios) - # ═══════════════════════════════════════════════════════════════════════════════ - Scenario( - id="internet_slow", - name="Internet Slower Than Before", - description="Customer 252 experiencing slow internet on 1Gbps tier", - customer_id=252, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 252. My internet has been really slow lately. I'm paying for 1Gbps but it feels much slower.", - expected_tool_calls=["get_subscription_detail", "get_support_tickets"], - expected_keywords=["speed", "issue", "incident", "troubleshoot"], - ), - ], - expected_tools=[ - "get_customer_detail", - "get_subscription_detail", - "get_support_tickets", - "search_knowledge", - ], - success_keywords=["speed", "troubleshoot", "reboot", "test", "incident"], - expected_resolution="Check subscription status, find open incident, provide troubleshooting steps", - ground_truth_solution="""The customer is on a 1Gbps plan but experiencing slow speeds. -Key facts to communicate: -1. There is an existing open service incident affecting the customer's area -2. The incident was reported on April 17 and is still under investigation -3. The service status shows 'slow' indicating a known issue - -Recommended actions: -- Acknowledge the known service issue and apologize for inconvenience -- Provide basic troubleshooting steps (restart router, check cables, test wired connection) -- Offer to create/escalate a support ticket for priority resolution -- Mention potential service credit once issue is resolved""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Identifies existing incident, provides troubleshooting steps, offers to escalate AND mentions potential credit -4 - Good: Identifies incident and provides troubleshooting, offers at least one proactive step -3 - Adequate: Acknowledges issue and provides some troubleshooting steps -2 - Poor: Generic troubleshooting without checking account status or incidents -1 - Fail: Unhelpful response or doesn't address the speed issue""", - ), - - Scenario( - id="internet_upgrade_inquiry", - name="Internet Speed Upgrade Options", - description="Customer wants to upgrade internet speed", - customer_id=25, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 25. I work from home and my current internet is too slow for video calls. What upgrade options do I have?", - expected_tool_calls=["get_subscription_detail", "get_products"], - expected_keywords=["upgrade", "speed", "plan", "price"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_products", "search_knowledge"], - success_keywords=["upgrade", "Mbps", "Gbps", "Pro", "Ultimate", "price"], - expected_resolution="Check current plan, show available upgrade options with pricing", - ground_truth_solution="""Help customer upgrade their internet plan. -Key information to provide: -1. Current plan details (speed tier, price) -2. Available upgrade options: - - Fiber Internet - Basic: 100 Mbps @ $49.99/month - - Fiber Internet - Pro: 500 Mbps @ $79.99/month - - Fiber Internet - Ultimate: 1 Gbps @ $119.99/month -3. For video calls, recommend at least Pro (500 Mbps) - -Recommended actions: -- Show price difference from current plan -- Explain upgrade benefits (WiFi 6 router, priority support) -- Offer any applicable promotions (loyalty upgrade, new customer discount) -- Upgrades take effect within 24 hours""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows current plan, presents upgrade options with pricing, recommends based on need, mentions promotions -4 - Good: Shows options with pricing and makes recommendation -3 - Adequate: Lists upgrade options but missing personalization -2 - Poor: Generic product info without checking current plan -1 - Fail: Doesn't provide helpful upgrade information""", - ), - - Scenario( - id="internet_router_reset", - name="Router Reset Help", - description="Customer needs help resetting their router", - customer_id=30, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 30. My router isn't working and I think I need to reset it. How do I do that?", - expected_tool_calls=["search_knowledge"], - expected_keywords=["reset", "button", "router"], - ), - ], - expected_tools=["get_customer_detail", "search_knowledge"], - success_keywords=["reset", "button", "10 seconds", "paperclip", "factory", "settings"], - expected_resolution="Provide step-by-step router reset instructions from knowledge base", - ground_truth_solution="""Help customer reset their router. -Steps to communicate: -1. Locate the reset button on the back of the router -2. Use a paperclip to press and hold the button for 10 seconds -3. Wait for the router to restart (lights will blink) -4. Router returns to factory settings -5. Reconnect using default WiFi name and password on router label - -Additional help: -- If issues persist after reset, contact support at 1-800-CONTOSO -- Offer to schedule a technician if customer is uncomfortable doing it themselves""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Provides complete step-by-step instructions, mentions factory settings warning, offers additional help -4 - Good: Provides reset steps and basic guidance -3 - Adequate: Gives reset instructions but incomplete -2 - Poor: Vague instructions without specific steps -1 - Fail: Doesn't help with router reset""", - ), - - Scenario( - id="internet_outage_report", - name="Internet Outage Report", - description="Customer reporting complete internet outage", - customer_id=35, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 35. My internet is completely down! Nothing is working. Is there an outage in my area?", - expected_tool_calls=["get_subscription_detail", "get_support_tickets"], - expected_keywords=["outage", "incident", "status"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge"], - success_keywords=["outage", "incident", "ticket", "technician", "status"], - expected_resolution="Check for area outages, create support ticket if needed, provide ETA", - ground_truth_solution="""Handle internet outage report. -Key actions: -1. Check subscription service status -2. Look for existing service incidents -3. Check if other support tickets exist for this customer - -If outage confirmed: -- Apologize for the inconvenience -- Provide estimated restoration time -- Offer to notify when service is restored - -If no known outage: -- Create a new support ticket -- Provide troubleshooting steps -- Offer technician visit if needed -- Mention service credit for extended outages""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Checks outage status, creates ticket if needed, provides ETA, offers follow-up -4 - Good: Checks status and takes appropriate action -3 - Adequate: Acknowledges issue and offers some help -2 - Poor: Generic response without checking system -1 - Fail: Doesn't address the outage report""", - ), - - Scenario( - id="internet_static_ip", - name="Static IP Request", - description="Customer needs a static IP address for work", - customer_id=40, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 40. I need a static IP address for my home server. Do you offer that?", - expected_tool_calls=["get_subscription_detail", "get_products"], - expected_keywords=["static", "IP", "feature"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], - success_keywords=["static", "IP", "Pro", "Ultimate", "upgrade", "feature"], - expected_resolution="Check current plan, explain static IP is included in Pro/Ultimate plans", - ground_truth_solution="""Help customer get a static IP address. -Key information: -1. Static IP is included in: - - Fiber Internet - Pro ($79.99/month) - includes 1 static IP - - Fiber Internet - Ultimate ($119.99/month) - includes 1 static IP - - Business Internet - Enterprise ($299.99/month) - includes static IP block -2. Basic plan does not include static IP - -Recommended actions: -- Check current plan -- If on Basic, recommend upgrade to Pro -- Explain static IP benefits and configuration -- Offer to process upgrade immediately""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Checks plan, explains which plans include static IP, recommends appropriate option -4 - Good: Explains static IP availability and recommends upgrade -3 - Adequate: Mentions static IP but missing plan details -2 - Poor: Generic response without specific information -1 - Fail: Doesn't address the static IP request""", - ), - - # ═══════════════════════════════════════════════════════════════════════════════ - # MOBILE & ROAMING SCENARIOS (4 scenarios) - # ═══════════════════════════════════════════════════════════════════════════════ - Scenario( - id="roaming_travel", - name="Travelling Abroad - Needs Roaming", - description="Customer 253 traveling to Spain, needs roaming setup", - customer_id=253, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 253. I'm traveling to Spain in 2 days and need to know about international roaming.", - expected_tool_calls=["get_subscription_detail", "get_products"], - expected_keywords=["roaming", "international", "activate"], - ), - ], - expected_tools=[ - "get_customer_detail", - "get_subscription_detail", - "get_products", - "search_knowledge", - ], - success_keywords=["roaming", "international", "activate", "add-on"], - expected_resolution="Check roaming not enabled, suggest International Roaming add-on, explain 3-day activation requirement", - ground_truth_solution="""The customer needs international roaming enabled before traveling to Spain in 2 days. -Key facts to communicate: -1. International roaming is currently NOT enabled on their account -2. Roaming packages typically require 3 days to activate (customer is cutting it close) -3. Spain is covered under European roaming options -4. Available add-ons include voice, text, and data packages - -Recommended actions: -- Urgently enable international roaming on the account -- Recommend appropriate roaming package for Spain (Europe zone) -- Warn about the activation timeline (may need to request expedited activation) -- Explain roaming rates and usage alerts to avoid bill shock""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Identifies roaming is off, explains urgency (3-day activation), offers to enable AND recommends specific package -4 - Good: Identifies roaming status and urgency, offers to enable roaming -3 - Adequate: Identifies roaming is not enabled and offers to help activate -2 - Poor: Generic roaming information without checking account status -1 - Fail: Doesn't address the roaming request or provides incorrect information""", - ), - - Scenario( - id="mobile_data_usage", - name="Mobile Data Usage Check", - description="Customer wants to check mobile data usage before cycle ends", - customer_id=45, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 45. How much data have I used this month? I don't want to go over my limit.", - expected_tool_calls=["get_data_usage", "get_subscription_detail"], - expected_keywords=["data", "usage", "GB", "limit"], - ), - ], - expected_tools=["get_customer_detail", "get_data_usage", "get_subscription_detail"], - success_keywords=["data", "used", "remaining", "GB", "cap"], - expected_resolution="Show current data usage vs plan limit, warn if close to limit", - ground_truth_solution="""Check customer's data usage. -Key information to provide: -1. Current data usage for this billing cycle -2. Data cap from subscription plan -3. Days remaining in billing cycle -4. Percentage of data used - -If close to limit: -- Warn about overage charges -- Suggest data-saving tips -- Offer unlimited data upgrade option -- Explain how to set up usage alerts""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows usage, cap, remaining, and provides proactive advice based on status -4 - Good: Shows usage and limit with clear comparison -3 - Adequate: Provides data usage information -2 - Poor: Vague or incomplete information -1 - Fail: Doesn't provide data usage""", - ), - - Scenario( - id="mobile_upgrade_premium", - name="Mobile Plan Upgrade", - description="Customer wants to upgrade mobile plan for more data", - customer_id=3, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 3. I keep running out of data. What mobile plans with more data do you have?", - expected_tool_calls=["get_subscription_detail", "get_products"], - expected_keywords=["plan", "unlimited", "data", "price"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], - success_keywords=["Premium", "unlimited", "data", "59.99", "upgrade"], - expected_resolution="Show current plan, recommend Mobile Plan - Premium with unlimited data", - ground_truth_solution="""Help customer upgrade mobile plan. -Key information: -1. Current plan: Mobile Plan - Essential (5GB data @ $29.99/month) -2. Recommended upgrade: Mobile Plan - Premium ($59.99/month) - - Unlimited data - - International roaming included - - 5G Priority - - 50GB hotspot - -Recommended actions: -- Explain price difference ($30/month more) -- Highlight unlimited data benefit -- Mention included international roaming -- Offer to process upgrade immediately""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows current plan, recommends Premium with pricing, highlights benefits -4 - Good: Provides upgrade options with clear comparison -3 - Adequate: Mentions upgrade options but missing details -2 - Poor: Generic product info without personalization -1 - Fail: Doesn't help with upgrade""", - ), - - Scenario( - id="mobile_hotspot_question", - name="Mobile Hotspot Inquiry", - description="Customer asking about hotspot feature on their plan", - customer_id=8, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 8. Does my mobile plan include hotspot? I need to use it for my laptop.", - expected_tool_calls=["get_subscription_detail"], - expected_keywords=["hotspot", "plan", "feature"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], - success_keywords=["hotspot", "included", "GB", "tethering"], - expected_resolution="Check plan details, explain hotspot inclusion based on plan tier", - ground_truth_solution="""Answer hotspot question. -Key information based on mobile plan: -- Essential plan: Hotspot NOT included (or limited) -- Premium plan: 50GB hotspot included - -Actions: -- Check customer's current mobile plan -- Explain hotspot feature availability -- If not included, offer Premium upgrade -- Provide instructions on enabling hotspot if available""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Checks plan, explains hotspot status, provides usage info or upgrade option -4 - Good: Explains hotspot availability for their plan -3 - Adequate: Addresses hotspot question generally -2 - Poor: Vague response without checking plan -1 - Fail: Doesn't address hotspot question""", - ), - - # ═══════════════════════════════════════════════════════════════════════════════ - # ACCOUNT & SECURITY SCENARIOS (4 scenarios) - # ═══════════════════════════════════════════════════════════════════════════════ - Scenario( - id="account_locked", - name="Account Locked After Failed Logins", - description="Customer 254 locked out after multiple failed login attempts", - customer_id=254, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 254. I can't log into my account - it says it's locked!", - expected_tool_calls=["get_security_logs", "unlock_account"], - expected_keywords=["locked", "security", "unlock", "password"], - ), - ], - expected_tools=[ - "get_customer_detail", - "get_security_logs", - "unlock_account", - "search_knowledge", - ], - success_keywords=["unlock", "password", "security", "2FA", "reset"], - expected_resolution="Query security logs, verify identity, unlock account, recommend password reset and 2FA", - ground_truth_solution="""The customer's account is locked due to multiple failed login attempts. -Key facts to communicate: -1. Security logs show multiple failed login attempts triggering automatic lockout -2. This is a security feature to protect the account -3. The account can be unlocked after identity verification - -Required actions: -- Verify customer identity (already done via customer ID) -- Unlock the account using unlock_account tool -- Confirm the account is now accessible - -Recommended follow-up: -- Suggest password reset if customer forgot password -- Recommend enabling 2FA for additional security -- Advise using password manager to prevent future lockouts""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Verifies identity, unlocks account, confirms success, AND provides security recommendations (password reset, 2FA) -4 - Good: Verifies identity, unlocks account, and provides at least one security recommendation -3 - Adequate: Unlocks the account and confirms it's accessible -2 - Poor: Attempts to help but doesn't actually unlock the account -1 - Fail: Doesn't address the lockout or provides incorrect instructions""", - ), - - Scenario( - id="account_security_check", - name="Security Audit Request", - description="Customer concerned about account security after hearing about data breaches", - customer_id=12, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 12. I heard about data breaches in the news. Can you check if my account is secure?", - expected_tool_calls=["get_security_logs"], - expected_keywords=["security", "login", "access"], - ), - ], - expected_tools=["get_customer_detail", "get_security_logs", "search_knowledge"], - success_keywords=["security", "login", "attempts", "2FA", "password"], - expected_resolution="Review security logs, confirm no suspicious activity, recommend security best practices", - ground_truth_solution="""Perform security audit for customer. -Key actions: -1. Review security logs for suspicious activity -2. Check for failed login attempts from unknown locations -3. Verify no unauthorized access - -Provide security recommendations: -- Enable 2FA if not already enabled -- Use strong, unique password -- Update password every 90 days -- Never share credentials -- Monitor account for suspicious activity - -Reassure customer and explain security measures in place.""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Reviews logs, reports findings, provides comprehensive security recommendations -4 - Good: Checks security status and provides recommendations -3 - Adequate: Reviews security but limited recommendations -2 - Poor: Generic security advice without checking account -1 - Fail: Doesn't address security concern""", - ), - - Scenario( - id="account_update_contact", - name="Update Contact Information", - description="Customer wants to update email and phone number", - customer_id=18, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 18. I have a new email and phone number. Can you update my account information?", - expected_tool_calls=["get_customer_detail"], - expected_keywords=["update", "contact", "email", "phone"], - ), - ], - expected_tools=["get_customer_detail"], - success_keywords=["update", "email", "phone", "verify", "confirm"], - expected_resolution="Show current info, explain update process, request new details", - ground_truth_solution="""Help customer update contact information. -Key actions: -1. Retrieve current contact details -2. Verify customer identity -3. Collect new email and phone number -4. Explain verification process for new contact info - -Security note: -- New email may require verification -- Update affects notifications and billing alerts -- Password reset links go to email on file -- Confirm all communication preferences""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows current info, requests new details, explains verification, updates preferences -4 - Good: Helps with update and explains process -3 - Adequate: Acknowledges request and provides guidance -2 - Poor: Generic response without checking current info -1 - Fail: Doesn't help with update""", - ), - - Scenario( - id="account_paperless_billing", - name="Paperless Billing Setup", - description="Customer wants to switch to paperless billing", - customer_id=22, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 22. I want to go paperless and stop receiving paper bills. How do I do that?", - expected_tool_calls=["get_customer_detail"], - expected_keywords=["paperless", "email", "billing"], - ), - ], - expected_tools=["get_customer_detail", "search_knowledge"], - success_keywords=["paperless", "email", "billing", "enabled", "notification"], - expected_resolution="Check current settings, enable paperless billing, confirm email on file", - ground_truth_solution="""Enable paperless billing for customer. -Key actions: -1. Check current billing preferences -2. Verify email address on file -3. Enable paperless billing -4. Explain paperless billing benefits - -Confirm: -- Bills will be sent to email on file -- Paper bills will stop within 1-2 billing cycles -- Can view all bills online anytime -- Email notifications for new bills""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Checks settings, confirms email, enables paperless, explains benefits -4 - Good: Enables paperless and confirms changes -3 - Adequate: Provides guidance on paperless billing -2 - Poor: Generic info without checking account -1 - Fail: Doesn't help with paperless setup""", - ), - - # ═══════════════════════════════════════════════════════════════════════════════ - # TV & STREAMING SCENARIOS (2 scenarios) - # ═══════════════════════════════════════════════════════════════════════════════ - Scenario( - id="tv_channel_lineup", - name="TV Channel Lineup Question", - description="Customer asking about available channels on their TV plan", - customer_id=28, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 28. What channels do I get with my TV streaming plan?", - expected_tool_calls=["get_subscription_detail", "get_products"], - expected_keywords=["channels", "TV", "streaming"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], - success_keywords=["channels", "streaming", "screens", "replay"], - expected_resolution="Check TV subscription, list included channels and features", - ground_truth_solution="""Show TV streaming plan details. -TV Streaming plans: -- Basic ($34.99/month): 50+ channels, 2 screens, 7-day replay -- Premium ($64.99/month): 150+ channels, 4 screens, 30-day replay, sports, movies - -Actions: -- Check current TV subscription -- List included channels/features -- Mention upgrade options if on Basic -- Explain how to access streaming app""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows plan details, lists features, mentions upgrade if applicable -4 - Good: Explains included channels and features -3 - Adequate: Provides plan information -2 - Poor: Generic TV info without checking plan -1 - Fail: Doesn't address channel question""", - ), - - Scenario( - id="tv_add_sports", - name="Add Sports Package", - description="Customer wants to add sports channels to TV plan", - customer_id=32, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 32. I want to watch football games. Do you have a sports package I can add?", - expected_tool_calls=["get_subscription_detail", "get_products"], - expected_keywords=["sports", "package", "channels"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], - success_keywords=["sports", "Premium", "channels", "upgrade"], - expected_resolution="Check current TV plan, explain sports is in Premium, offer upgrade", - ground_truth_solution="""Help customer add sports channels. -Key information: -- Sports package is included in TV Streaming - Premium ($64.99/month) -- Basic plan does not include sports channels - -Actions: -- Check current TV subscription -- If on Basic, offer upgrade to Premium -- Premium includes sports package plus movie channels -- Also includes 4 screens and 30-day replay -- Calculate price difference from current plan""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Checks plan, explains sports in Premium, shows pricing, offers to upgrade -4 - Good: Explains sports availability and upgrade option -3 - Adequate: Mentions sports package info -2 - Poor: Generic info without checking current plan -1 - Fail: Doesn't help with sports request""", - ), - - # ═══════════════════════════════════════════════════════════════════════════════ - # BUNDLE & PROMOTION SCENARIOS (3 scenarios) - # ═══════════════════════════════════════════════════════════════════════════════ - Scenario( - id="bundle_inquiry", - name="Bundle Package Inquiry", - description="Customer interested in bundling services", - customer_id=38, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 38. I have internet and mobile separately. Would I save money if I bundle them?", - expected_tool_calls=["get_subscription_detail", "get_products"], - expected_keywords=["bundle", "save", "discount"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], - success_keywords=["bundle", "Family Complete", "discount", "save", "$199.99"], - expected_resolution="Show current services, calculate potential savings with bundle", - ground_truth_solution="""Help customer understand bundle savings. -Bundle option: -- Bundle - Family Complete: $199.99/month - - 500Mbps Internet - - 150+ TV Channels - - 2 Unlimited Mobile Lines - - 20% discount vs individual services - -Actions: -- Check current subscriptions and total cost -- Calculate potential savings with bundle -- Explain bundle includes more than current services -- Show value proposition -- Offer to switch to bundle if interested""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows current cost, calculates savings, explains bundle benefits, offers to switch -4 - Good: Explains bundle options and potential savings -3 - Adequate: Provides bundle information -2 - Poor: Generic bundle info without checking current services -1 - Fail: Doesn't help with bundle inquiry""", - ), - - Scenario( - id="promotion_eligibility", - name="Promotion Eligibility Check", - description="Customer asking about current promotions they qualify for", - customer_id=42, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 42. Are there any promotions or discounts I'm eligible for?", - expected_tool_calls=["get_customer_detail", "get_subscription_detail"], - expected_keywords=["promotion", "discount", "offer"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_products"], - success_keywords=["promotion", "discount", "loyalty", "offer", "eligible"], - expected_resolution="Check loyalty level, current services, find applicable promotions", - ground_truth_solution="""Check customer eligibility for promotions. -Available promotions: -1. New Customer - 20% Off (if new customer) -2. Bundle & Save - $50/month off (if 3+ services) -3. Loyalty Reward - Free speed upgrade (if Gold/Platinum) -4. Refer a Friend - $100 credit - -Actions: -- Check loyalty level (Bronze/Silver/Gold/Platinum) -- Check number of active services -- Identify applicable promotions -- Explain how to take advantage of offers""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Checks eligibility, lists applicable promos, explains how to apply -4 - Good: Identifies promotions customer qualifies for -3 - Adequate: Mentions available promotions -2 - Poor: Generic promo list without checking eligibility -1 - Fail: Doesn't help with promotion inquiry""", - ), - - Scenario( - id="loyalty_benefits", - name="Loyalty Program Benefits", - description="Customer asking about loyalty program benefits", - customer_id=48, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 48. I've been with you for years. What loyalty benefits do I get?", - expected_tool_calls=["get_customer_detail"], - expected_keywords=["loyalty", "benefits", "level"], - ), - ], - expected_tools=["get_customer_detail", "get_products", "search_knowledge"], - success_keywords=["loyalty", "Gold", "Silver", "Platinum", "benefits", "upgrade"], - expected_resolution="Check loyalty level, explain tier benefits, mention upgrade path", - ground_truth_solution="""Show loyalty program benefits. -Loyalty tiers: -- Bronze: Basic support -- Silver: Priority support, occasional discounts -- Gold: 24/7 VIP support, free speed upgrades, special promotions -- Platinum: All Gold benefits plus dedicated account manager - -Actions: -- Check customer's current loyalty level -- Explain benefits of their tier -- Mention how to reach next tier -- Highlight current Gold/Platinum promotion (free speed upgrade)""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Shows loyalty level, explains tier benefits, mentions upgrade path and current promos -4 - Good: Explains loyalty benefits for their tier -3 - Adequate: Provides loyalty program info -2 - Poor: Generic loyalty info without checking level -1 - Fail: Doesn't address loyalty question""", - ), - - # ═══════════════════════════════════════════════════════════════════════════════ - # SUPPORT TICKET SCENARIOS (2 scenarios) - # ═══════════════════════════════════════════════════════════════════════════════ - Scenario( - id="support_ticket_status", - name="Support Ticket Status Check", - description="Customer checking status of existing support ticket", - customer_id=6, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 6. I opened a support ticket a few days ago. Can you check the status?", - expected_tool_calls=["get_support_tickets"], - expected_keywords=["ticket", "status", "open"], - ), - ], - expected_tools=["get_customer_detail", "get_support_tickets"], - success_keywords=["ticket", "status", "open", "pending", "resolved"], - expected_resolution="Find open tickets, provide status update, explain next steps", - ground_truth_solution="""Check support ticket status. -Key actions: -1. Look up open/pending support tickets -2. Provide ticket number and status -3. Explain current stage of resolution -4. Provide expected resolution timeline - -If ticket is pending: -- Explain what's being done -- Offer to escalate if delayed -- Provide contact for urgent issues""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Finds ticket, shows status, explains next steps, offers to escalate if needed -4 - Good: Provides ticket status and explanation -3 - Adequate: Finds and reports ticket status -2 - Poor: Generic response without checking tickets -1 - Fail: Doesn't help with ticket status""", - ), - - Scenario( - id="support_new_ticket", - name="Create New Support Ticket", - description="Customer wanting to open a new support ticket for equipment issue", - customer_id=14, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 14. My cable box keeps rebooting randomly. I need someone to look at this.", - expected_tool_calls=["get_subscription_detail", "get_support_tickets"], - expected_keywords=["ticket", "equipment", "technician"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_support_tickets"], - success_keywords=["ticket", "equipment", "technician", "issue", "scheduled"], - expected_resolution="Document issue, create support ticket, offer technician visit", - ground_truth_solution="""Handle equipment issue and create ticket. -Key actions: -1. Document the cable box issue (random reboots) -2. Check subscription for equipment details -3. Basic troubleshooting: unplug for 30 seconds, check connections -4. If issue persists, create support ticket - -Support ticket should include: -- Equipment type and issue description -- Troubleshooting steps already attempted -- Priority level based on severity -- Offer technician visit if needed""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Documents issue, tries troubleshooting, creates ticket, offers technician -4 - Good: Creates ticket and offers resolution options -3 - Adequate: Acknowledges issue and offers to help -2 - Poor: Generic troubleshooting without creating ticket -1 - Fail: Doesn't address the equipment issue""", - ), - - # ═══════════════════════════════════════════════════════════════════════════════ - # MULTI-TURN SCENARIOS (5 scenarios with 2-4 turns each) - # ═══════════════════════════════════════════════════════════════════════════════ - Scenario( - id="multi_billing_dispute", - name="[Multi-Turn] Billing Dispute Resolution", - description="Customer disputes charge, agent investigates, customer asks for credit, then upgrade", - customer_id=7, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 7. There's a $50 charge on my bill I don't recognize. What is this for?", - expected_tool_calls=["get_billing_summary"], - expected_keywords=["charge", "invoice", "billing"], - ), - ConversationTurn( - user_message="I didn't order any equipment or additional services. Can you remove this charge?", - expected_tool_calls=[], - expected_keywords=["credit", "remove", "adjustment"], - ), - ConversationTurn( - user_message="Thanks for the credit. While I have you, are there any promotions I qualify for?", - expected_tool_calls=["get_customer_detail"], - expected_keywords=["promotion", "discount", "offer"], - ), - ], - expected_tools=["get_customer_detail", "get_billing_summary", "get_subscription_detail"], - success_keywords=["charge", "credit", "adjustment", "promotion", "discount"], - expected_resolution="Investigate charge, apply credit if warranted, then check promotion eligibility", - ground_truth_solution="""Multi-turn billing dispute resolution: - -Turn 1 - Investigate the charge: -- Pull up billing summary to identify the $50 charge -- Explain what the charge is for (equipment fee, one-time charge, etc.) -- Show when it was applied - -Turn 2 - Handle credit request: -- If charge is erroneous, apply credit -- If valid, explain why but offer goodwill credit if appropriate -- Confirm the adjustment will appear on next bill - -Turn 3 - Check promotions: -- Review customer loyalty level and current services -- Identify applicable promotions -- Recommend best options based on their profile""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Investigates charge thoroughly, handles credit appropriately, provides personalized promotion info -4 - Good: Addresses each turn adequately with relevant information -3 - Adequate: Responds to each turn but missing depth or personalization -2 - Poor: Misses context between turns or provides generic responses -1 - Fail: Fails to address the dispute or loses conversation context""", - ), - - Scenario( - id="multi_internet_troubleshoot", - name="[Multi-Turn] Internet Troubleshooting Flow", - description="Step-by-step troubleshooting: check status, try fixes, escalate to technician", - customer_id=16, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 16. My internet keeps dropping every few minutes. It's really frustrating.", - expected_tool_calls=["get_subscription_detail", "get_support_tickets"], - expected_keywords=["internet", "issue", "connection"], - ), - ConversationTurn( - user_message="I already tried restarting the router. It worked for a bit but started dropping again.", - expected_tool_calls=["search_knowledge"], - expected_keywords=["troubleshoot", "check", "cable"], - ), - ConversationTurn( - user_message="I checked the cables and they look fine. I think there might be something wrong with the equipment.", - expected_tool_calls=[], - expected_keywords=["technician", "appointment", "visit"], - ), - ConversationTurn( - user_message="Yes, please schedule a technician. What times are available?", - expected_tool_calls=[], - expected_keywords=["scheduled", "appointment", "confirm"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_support_tickets", "search_knowledge"], - success_keywords=["troubleshoot", "router", "cables", "technician", "appointment", "scheduled"], - expected_resolution="Progressive troubleshooting leading to technician scheduling", - ground_truth_solution="""Multi-turn troubleshooting flow: - -Turn 1 - Initial diagnosis: -- Check subscription and service status -- Look for existing incidents or tickets -- Acknowledge the issue and express empathy - -Turn 2 - Continue troubleshooting: -- Since router restart was tried, suggest next steps -- Check cable connections -- Try wired connection to isolate WiFi vs line issue -- Check for interference - -Turn 3 - Escalate to technician: -- Acknowledge customer has tried basic troubleshooting -- Agree equipment may need inspection -- Offer to schedule technician visit - -Turn 4 - Schedule appointment: -- Offer available time slots -- Confirm appointment details -- Provide technician arrival window -- Mention what technician will check""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Progressive troubleshooting, builds on previous turns, smooth escalation to technician -4 - Good: Addresses each step appropriately, schedules technician -3 - Adequate: Follows the flow but may skip steps or lack continuity -2 - Poor: Repetitive suggestions or doesn't build on previous attempts -1 - Fail: Doesn't progress logically or fails to schedule technician""", - ), - - Scenario( - id="multi_service_cancellation", - name="[Multi-Turn] Service Cancellation Retention", - description="Customer wants to cancel, agent attempts retention with offers", - customer_id=24, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 24. I want to cancel my internet service. It's too expensive.", - expected_tool_calls=["get_subscription_detail", "get_billing_summary"], - expected_keywords=["cancel", "service", "understand"], - ), - ConversationTurn( - user_message="I've been paying $119 a month and I found a competitor offering $70 for similar speeds.", - expected_tool_calls=["get_products"], - expected_keywords=["offer", "discount", "match", "retention"], - ), - ConversationTurn( - user_message="A 20% discount sounds good. What would my new monthly rate be?", - expected_tool_calls=[], - expected_keywords=["$95", "monthly", "rate", "discount"], - ), - ], - expected_tools=["get_customer_detail", "get_subscription_detail", "get_billing_summary", "get_products"], - success_keywords=["cancel", "retention", "discount", "offer", "rate", "save"], - expected_resolution="Understand cancellation reason, offer retention discount, retain customer", - ground_truth_solution="""Multi-turn retention flow: - -Turn 1 - Understand cancellation reason: -- Pull up subscription details and billing -- Express understanding about cost concerns -- Ask about their specific needs -- Don't immediately accept cancellation - -Turn 2 - Make retention offer: -- Acknowledge competitor pricing -- Check for available retention offers -- Offer 20% loyalty discount or price match -- Highlight value-adds (speed, reliability, support) - -Turn 3 - Close the retention: -- Calculate new rate with discount ($119 × 0.8 = $95.20) -- Confirm the discount will be applied -- Explain discount duration (12 months, etc.) -- Thank customer for staying""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Empathetic handling, competitive retention offer, calculates new rate, secures retention -4 - Good: Makes appropriate retention offer and calculates savings -3 - Adequate: Attempts retention but may miss personalization or calculation -2 - Poor: Too quick to cancel or weak retention attempt -1 - Fail: Processes cancellation without retention effort""", - ), - - Scenario( - id="multi_new_customer_setup", - name="[Multi-Turn] New Service Setup Assistance", - description="Customer needs help choosing and setting up new services", - customer_id=2, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 2. I just moved to a new apartment and need to set up internet. What are my options?", - expected_tool_calls=["get_products"], - expected_keywords=["internet", "plans", "options"], - ), - ConversationTurn( - user_message="I work from home and need reliable internet for video calls. Which plan do you recommend?", - expected_tool_calls=["get_subscription_detail"], - expected_keywords=["Pro", "500Mbps", "recommend"], - ), - ConversationTurn( - user_message="The Pro plan sounds good. Do you have any current promotions for new setups?", - expected_tool_calls=[], - expected_keywords=["promotion", "discount", "new customer"], - ), - ConversationTurn( - user_message="Great! Please set me up with the Pro plan and the new customer discount.", - expected_tool_calls=[], - expected_keywords=["confirm", "order", "setup", "welcome"], - ), - ], - expected_tools=["get_customer_detail", "get_products", "get_subscription_detail"], - success_keywords=["internet", "Pro", "500Mbps", "promotion", "discount", "setup", "order"], - expected_resolution="Guide through plan selection, apply promotion, complete setup", - ground_truth_solution="""Multi-turn new customer setup: - -Turn 1 - Present options: -- List available internet plans (Basic, Pro, Ultimate) -- Explain speed tiers and pricing -- Ask about usage needs - -Turn 2 - Make recommendation: -- For WFH with video calls, recommend Pro (500 Mbps) -- Explain why it's suitable (consistent speed, WiFi 6, priority support) -- Mention Ultimate if they want overkill - -Turn 3 - Present promotions: -- New Customer 20% off first 3 months -- Mention WiFi 6 router included -- Explain installation options - -Turn 4 - Complete setup: -- Confirm plan selection (Pro @ $79.99) -- Apply 20% promotion (first 3 months = $63.99) -- Set installation date -- Welcome to Contoso""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Natural sales flow, personalized recommendation, applies promo, completes setup smoothly -4 - Good: Guides through selection and setup with appropriate recommendations -3 - Adequate: Completes setup but may lack personalization or miss promotion -2 - Poor: Disjointed experience or missing key steps -1 - Fail: Doesn't complete the setup or loses track of conversation""", - ), - - Scenario( - id="multi_complex_account_issue", - name="[Multi-Turn] Complex Account Resolution", - description="Customer has multiple issues: wrong charge, slow internet, and needs plan change", - customer_id=11, - turns=[ - ConversationTurn( - user_message="Hi, I'm customer 11. I have several issues. First, I was charged for a service I cancelled last month.", - expected_tool_calls=["get_billing_summary", "get_subscription_detail"], - expected_keywords=["charge", "cancelled", "billing"], - ), - ConversationTurn( - user_message="Also, my internet has been slow for the past week. Are there any known issues?", - expected_tool_calls=["get_support_tickets"], - expected_keywords=["slow", "internet", "incident", "issue"], - ), - ConversationTurn( - user_message="One more thing - I want to downgrade my TV package. I don't watch that much anymore.", - expected_tool_calls=["get_products"], - expected_keywords=["downgrade", "TV", "package", "change"], - ), - ConversationTurn( - user_message="Can you summarize all the changes you're making to my account?", - expected_tool_calls=[], - expected_keywords=["summary", "credit", "downgrade", "changes"], - ), - ], - expected_tools=["get_customer_detail", "get_billing_summary", "get_subscription_detail", "get_support_tickets", "get_products"], - success_keywords=["credit", "refund", "slow", "incident", "downgrade", "TV", "summary", "changes"], - expected_resolution="Handle billing credit, check internet issue, process TV downgrade, summarize all changes", - ground_truth_solution="""Multi-turn complex account resolution: - -Turn 1 - Billing issue: -- Check billing for the cancelled service charge -- Identify the erroneous charge -- Apply credit/refund for the amount -- Confirm it will be removed - -Turn 2 - Internet issue: -- Check for service incidents -- Check subscription service status -- If incident exists, provide status and ETA -- If not, offer troubleshooting - -Turn 3 - TV downgrade: -- Show current TV package -- Explain downgrade options (Premium to Basic) -- Calculate savings -- Process the change - -Turn 4 - Summary: -- Recap all changes made: - 1. Credit applied for erroneous charge: $X - 2. Internet issue: status/resolution - 3. TV downgrade: from Premium to Basic, saving $X/month -- Confirm customer is satisfied""", - scoring_rubric="""Score 1-5 based on these criteria: -5 - Excellent: Handles all 3 issues effectively, provides clear summary, maintains context throughout -4 - Good: Addresses all issues with reasonable resolution -3 - Adequate: Handles most issues but may miss one or lack cohesive summary -2 - Poor: Loses track of issues or provides incomplete resolution -1 - Fail: Unable to handle multiple issues or forgets earlier requests""", - ), -] - - -# ═══════════════════════════════════════════════════════════════════════════════ -# SCENARIO EVALUATOR (Using AgentTestRunner) -# ═══════════════════════════════════════════════════════════════════════════════ - - -@dataclass -class ScenarioResult: - """Complete result from running a scenario.""" - scenario: Scenario - agent_name: str - query_results: list[QueryResult] = field(default_factory=list) - total_time: float = 0.0 - - # Tool-based metrics (Process Evaluation) - Rule-based F1 - tool_recall: float = 0.0 # % of expected tools called - tool_precision: float = 0.0 # % of called tools that were expected - tool_f1: float = 0.0 # Harmonic mean of precision and recall - - # Outcome-based metrics (Goal Evaluation) - Keyword matching fallback - keyword_coverage: float = 0.0 # % of success keywords in response - response_length: int = 0 # Total response length - - # LLM-as-Judge metrics (Azure AI Foundry) - llm_intent_score: Optional[float] = None # 1-5: Did agent understand intent? - llm_intent_result: Optional[str] = None # "pass" or "fail" - llm_intent_reason: Optional[str] = None - llm_task_score: Optional[float] = None # 1-5: Did response follow task? - llm_task_result: Optional[str] = None - llm_task_reason: Optional[str] = None - llm_tool_score: Optional[float] = None # 1-5: Were correct tools called? - llm_tool_result: Optional[str] = None - llm_coherence: Optional[float] = None # 1-5: Is response coherent? - llm_fluency: Optional[float] = None # 1-5: Is language natural? - llm_relevance: Optional[float] = None # 1-5: Is response relevant? - llm_solution_score: Optional[float] = None # 1-5: Solution accuracy vs ground truth - llm_solution_reason: Optional[str] = None # Explanation of solution score - llm_eval_time: float = 0.0 - llm_errors: list[str] = field(default_factory=list) - - # Overall - success: bool = False - - def compute_metrics(self): - """Compute both tool accuracy and outcome metrics (rule-based).""" - # Collect all tools and responses - all_tools_called = set() - all_responses = [] - - for qr in self.query_results: - all_tools_called.update(qr.tool_calls) - all_responses.append(qr.response.lower()) - - combined_response = " ".join(all_responses) - self.response_length = len(combined_response) - - # ───────────────────────────────────────────────────────────────────── - # TOOL ACCURACY (Process-Based) - Rule-based F1 - # ───────────────────────────────────────────────────────────────────── - expected_tools = set(self.scenario.expected_tools) - - if expected_tools: - # Recall: What % of expected tools were called? - self.tool_recall = len(all_tools_called & expected_tools) / len(expected_tools) - - if all_tools_called: - # Precision: What % of called tools were expected? - self.tool_precision = len(all_tools_called & expected_tools) / len(all_tools_called) - - # F1 Score: Harmonic mean - if self.tool_precision + self.tool_recall > 0: - self.tool_f1 = 2 * (self.tool_precision * self.tool_recall) / (self.tool_precision + self.tool_recall) - - # ───────────────────────────────────────────────────────────────────── - # KEYWORD COVERAGE (Outcome-Based) - Fallback when LLM judge not used - # ───────────────────────────────────────────────────────────────────── - if self.scenario.success_keywords: - found = sum(1 for kw in self.scenario.success_keywords if kw.lower() in combined_response) - self.keyword_coverage = found / len(self.scenario.success_keywords) - - # ───────────────────────────────────────────────────────────────────── - # SUCCESS CRITERIA - # ───────────────────────────────────────────────────────────────────── - # If LLM judge was used, use its results - if self.llm_intent_result is not None or self.llm_task_result is not None: - # LLM-based success: intent resolved or task adhered - llm_passes = [] - if self.llm_intent_result: - llm_passes.append(self.llm_intent_result == "pass") - if self.llm_task_result: - llm_passes.append(self.llm_task_result == "pass") - self.success = any(llm_passes) if llm_passes else self.keyword_coverage >= 0.5 - else: - # Keyword-based success - has_no_errors = all(qr.error is None for qr in self.query_results) - self.success = self.keyword_coverage >= 0.5 and has_no_errors - - async def compute_llm_metrics(self, evaluator: "LLMJudgeEvaluator"): - """Compute LLM-as-Judge metrics using Azure AI Foundry evaluators.""" - if not self.query_results: - return - - # Get the query and response - query = self.scenario.turns[0].user_message if self.scenario.turns else "" - response = self.query_results[-1].response if self.query_results else "" - - # Get tool calls from all turns - all_tool_calls = [] - for qr in self.query_results: - for tool_name in qr.tool_calls: - all_tool_calls.append(ToolCall(name=tool_name)) - - # Convert MCP tool definitions - tool_defs = [ - ToolDefinition(name=td["name"], description=td["description"]) - for td in MCP_TOOL_DEFINITIONS - ] - - # Run LLM evaluation - try: - result = await evaluator.evaluate( - query=query, - response=response, - tool_calls=all_tool_calls, - tool_definitions=tool_defs, - ground_truth_solution=self.scenario.ground_truth_solution, - scoring_rubric=self.scenario.scoring_rubric, - ) - - # Copy results - self.llm_intent_score = result.intent_resolution_score - self.llm_intent_result = result.intent_resolution_result - self.llm_intent_reason = result.intent_resolution_reason - self.llm_task_score = result.task_adherence_score - self.llm_task_result = result.task_adherence_result - self.llm_task_reason = result.task_adherence_reason - self.llm_tool_score = result.tool_call_accuracy_score - self.llm_tool_result = result.tool_call_accuracy_result - self.llm_coherence = result.coherence_score - self.llm_fluency = result.fluency_score - self.llm_relevance = result.relevance_score - self.llm_solution_score = result.solution_accuracy_score - self.llm_solution_reason = result.solution_accuracy_reason - self.llm_eval_time = result.evaluation_time - self.llm_errors = result.errors - - except Exception as e: - self.llm_errors.append(f"LLM evaluation failed: {e}") - - -class ScenarioEvaluator: - """ - Runs scenarios against any agent using the generic AgentTestRunner. - Evaluates both tool accuracy and outcome quality. - - Supports two evaluation modes: - 1. Rule-based (default): Tool F1 + keyword matching - 2. LLM-as-Judge: Azure AI Foundry evaluators (IntentResolution, TaskAdherence, etc.) - """ - - def __init__( - self, - agent_name: str = "single", - use_llm_judge: bool = None, - enable_quality_metrics: bool = True, - ): - """ - Args: - agent_name: Agent shorthand ("single", "reflection", "handoff", "magentic") - or full module path - use_llm_judge: Use LLM-as-Judge evaluators (default: from env var) - enable_quality_metrics: Include coherence, fluency, relevance (slower) - """ - self.agent_name = agent_name - self.runner = AgentTestRunner(agent_name) - - # Determine if LLM judge should be used - if use_llm_judge is None: - use_llm_judge = USE_LLM_JUDGE and LLM_JUDGE_AVAILABLE - - self.use_llm_judge = use_llm_judge - self.enable_quality_metrics = enable_quality_metrics - self.llm_evaluator = None - - if self.use_llm_judge and LLM_JUDGE_AVAILABLE: - self.llm_evaluator = LLMJudgeEvaluator( - enable_agent_evaluators=True, - enable_quality_evaluators=enable_quality_metrics, - ) - print(f" [LLM] LLM-as-Judge: ENABLED") - else: - print(f" [RULE] LLM-as-Judge: DISABLED (using keyword matching)") - - async def run_scenario(self, scenario: Scenario) -> ScenarioResult: - """Run a complete scenario and return results.""" - result = ScenarioResult(scenario=scenario, agent_name=self.agent_name) - - start_time = time.time() - - # Run each turn in the scenario - for turn in scenario.turns: - query_result = await self.runner.run_query(turn.user_message) - result.query_results.append(query_result) - - result.total_time = time.time() - start_time - - # Compute rule-based metrics first - result.compute_metrics() - - # Optionally run LLM judge evaluation - if self.llm_evaluator is not None: - await result.compute_llm_metrics(self.llm_evaluator) - # Recompute success based on LLM results - result.compute_metrics() - - return result - - async def run_all_scenarios( - self, - scenarios: list[Scenario] | None = None, - verbose: bool = True, - ) -> list[ScenarioResult]: - """Run all scenarios and return results.""" - scenarios = scenarios or SCENARIOS - results = [] - - for scenario in scenarios: - if verbose: - print(f"\n[{self.agent_name}] Running: {scenario.name}") - - result = await self.run_scenario(scenario) - results.append(result) - - if verbose: - status = "✅" if result.success else "❌" - - # Show different metrics based on evaluation mode - if result.llm_intent_score is not None: - # LLM judge mode - intent = f"Intent: {result.llm_intent_score:.0f}/5" if result.llm_intent_score else "Intent: N/A" - task = f"Task: {result.llm_task_score:.0f}/5" if result.llm_task_score else "Task: N/A" - print(f" {status} {intent}, {task}, Time: {result.total_time:.1f}s (+{result.llm_eval_time:.1f}s eval)") - else: - # Keyword mode - print(f" {status} Tool F1: {result.tool_f1:.1%}, " - f"Keywords: {result.keyword_coverage:.1%}, " - f"Time: {result.total_time:.1f}s") - - return results - - -# ═══════════════════════════════════════════════════════════════════════════════ -# COMPARISON REPORT -# ═══════════════════════════════════════════════════════════════════════════════ - - -@dataclass -class AgentSummary: - """Aggregated metrics for an agent across all scenarios.""" - agent_name: str - scenarios_passed: int = 0 - scenarios_total: int = 0 - - # Rule-based metrics - avg_tool_recall: float = 0.0 - avg_tool_precision: float = 0.0 - avg_tool_f1: float = 0.0 - avg_keyword_coverage: float = 0.0 - - # LLM-as-Judge metrics - avg_intent_score: Optional[float] = None - avg_task_score: Optional[float] = None - avg_tool_score: Optional[float] = None - avg_coherence: Optional[float] = None - avg_fluency: Optional[float] = None - avg_relevance: Optional[float] = None - avg_solution_score: Optional[float] = None # Solution accuracy vs ground truth - intent_pass_rate: float = 0.0 - task_pass_rate: float = 0.0 - solution_pass_rate: float = 0.0 # % with solution score >= 3 - - avg_time: float = 0.0 - total_tools_called: int = 0 - uses_llm_judge: bool = False - - @classmethod - def from_results(cls, agent_name: str, results: list[ScenarioResult]) -> "AgentSummary": - if not results: - return cls(agent_name=agent_name) - - n = len(results) - - # Check if LLM judge was used - has_llm = any(r.llm_intent_score is not None for r in results) - - summary = cls( - agent_name=agent_name, - scenarios_passed=sum(1 for r in results if r.success), - scenarios_total=n, - avg_tool_recall=sum(r.tool_recall for r in results) / n, - avg_tool_precision=sum(r.tool_precision for r in results) / n, - avg_tool_f1=sum(r.tool_f1 for r in results) / n, - avg_keyword_coverage=sum(r.keyword_coverage for r in results) / n, - avg_time=sum(r.total_time for r in results) / n, - total_tools_called=sum(len(qr.tool_calls) for r in results for qr in r.query_results), - uses_llm_judge=has_llm, - ) - - # Compute LLM judge averages if available - if has_llm: - intent_scores = [r.llm_intent_score for r in results if r.llm_intent_score is not None] - task_scores = [r.llm_task_score for r in results if r.llm_task_score is not None] - tool_scores = [r.llm_tool_score for r in results if r.llm_tool_score is not None] - coherence = [r.llm_coherence for r in results if r.llm_coherence is not None] - fluency = [r.llm_fluency for r in results if r.llm_fluency is not None] - relevance = [r.llm_relevance for r in results if r.llm_relevance is not None] - solution_scores = [r.llm_solution_score for r in results if r.llm_solution_score is not None] - - if intent_scores: - summary.avg_intent_score = sum(intent_scores) / len(intent_scores) - if task_scores: - summary.avg_task_score = sum(task_scores) / len(task_scores) - if tool_scores: - summary.avg_tool_score = sum(tool_scores) / len(tool_scores) - if coherence: - summary.avg_coherence = sum(coherence) / len(coherence) - if fluency: - summary.avg_fluency = sum(fluency) / len(fluency) - if relevance: - summary.avg_relevance = sum(relevance) / len(relevance) - if solution_scores: - summary.avg_solution_score = sum(solution_scores) / len(solution_scores) - # Pass rate: score >= 3 (Adequate or better) - summary.solution_pass_rate = sum(1 for s in solution_scores if s >= 3) / len(solution_scores) - - # Pass rates - intent_passes = [r for r in results if r.llm_intent_result == "pass"] - task_passes = [r for r in results if r.llm_task_result == "pass"] - summary.intent_pass_rate = len(intent_passes) / n - summary.task_pass_rate = len(task_passes) / n - - return summary - - -def generate_comparison_report( - results_by_agent: dict[str, list[ScenarioResult]], -) -> str: - """Generate a comprehensive comparison report.""" - - # Check if LLM judge was used - first_results = list(results_by_agent.values())[0] - uses_llm = any(r.llm_intent_score is not None for r in first_results) - - mode = "LLM-as-Judge (Azure AI Foundry)" if uses_llm else "Rule-Based (Tool F1 + Keywords)" - - lines = [ - "", - "═" * 90, - f"AGENT EVALUATION REPORT: {mode}", - "═" * 90, - ] - - # Get agent names - agent_names = list(results_by_agent.keys()) - - # Per-scenario breakdown - lines.extend([ - "", - "SCENARIO BREAKDOWN", - "-" * 90, - ]) - - if uses_llm: - lines.append(f"{'Scenario':<28} {'Agent':<12} {'Pass':<6} {'Intent':<8} {'Solution':<10} {'Time':<8}") - else: - lines.append(f"{'Scenario':<28} {'Agent':<12} {'Pass':<6} {'Tool F1':<10} {'Keywords':<10} {'Time':<8}") - - lines.append("-" * 90) - - # Assume all agents ran same scenarios in same order - first_agent_results = results_by_agent[agent_names[0]] - - for i, scenario in enumerate([r.scenario for r in first_agent_results]): - scenario_name = scenario.name[:26] - - for agent_name in agent_names: - result = results_by_agent[agent_name][i] - status = "✅" if result.success else "❌" - - if uses_llm: - intent = f"{result.llm_intent_score:.0f}/5" if result.llm_intent_score else "N/A" - solution = f"{result.llm_solution_score:.0f}/5" if result.llm_solution_score else "N/A" - lines.append( - f"{scenario_name:<28} {agent_name:<12} {status:<6} " - f"{intent:<8} {solution:<10} {result.total_time:>6.1f}s" - ) - else: - lines.append( - f"{scenario_name:<28} {agent_name:<12} {status:<6} " - f"{result.tool_f1:>8.1%} {result.keyword_coverage:>10.1%} " - f"{result.total_time:>6.1f}s" - ) - - lines.append("") # Space between scenarios - - # Summary statistics - lines.extend([ - "-" * 90, - "SUMMARY", - "-" * 90, - "", - ]) - - # Header with agent names - header = f"{'Metric':<30}" - for name in agent_names: - header += f" {name:>15}" - lines.append(header) - lines.append("-" * (30 + 16 * len(agent_names))) - - # Compute summaries - summaries = {name: AgentSummary.from_results(name, results) - for name, results in results_by_agent.items()} - - # Metrics rows - different based on mode - if uses_llm: - metrics = [ - ("Scenarios Passed", lambda s: f"{s.scenarios_passed}/{s.scenarios_total}"), - ("Solution Pass Rate (>=3)", lambda s: f"{s.solution_pass_rate:.1%}" if s.avg_solution_score else "N/A"), - ("Avg Solution Score", lambda s: f"{s.avg_solution_score:.1f}/5" if s.avg_solution_score else "N/A"), - ("Avg Intent Score", lambda s: f"{s.avg_intent_score:.1f}/5" if s.avg_intent_score else "N/A"), - ("Avg Coherence", lambda s: f"{s.avg_coherence:.1f}/5" if s.avg_coherence else "N/A"), - ("Avg Fluency", lambda s: f"{s.avg_fluency:.1f}/5" if s.avg_fluency else "N/A"), - ("Avg Relevance", lambda s: f"{s.avg_relevance:.1f}/5" if s.avg_relevance else "N/A"), - ("Avg Time (s)", lambda s: f"{s.avg_time:.1f}"), - ("Total Tools Called", lambda s: f"{s.total_tools_called}"), - ] - else: - metrics = [ - ("Scenarios Passed", lambda s: f"{s.scenarios_passed}/{s.scenarios_total}"), - ("Avg Tool Recall", lambda s: f"{s.avg_tool_recall:.1%}"), - ("Avg Tool Precision", lambda s: f"{s.avg_tool_precision:.1%}"), - ("Avg Tool F1", lambda s: f"{s.avg_tool_f1:.1%}"), - ("Avg Keyword Coverage", lambda s: f"{s.avg_keyword_coverage:.1%}"), - ("Avg Time (s)", lambda s: f"{s.avg_time:.1f}"), - ("Total Tools Called", lambda s: f"{s.total_tools_called}"), - ] - - for metric_name, formatter in metrics: - row = f"{metric_name:<30}" - for name in agent_names: - row += f" {formatter(summaries[name]):>15}" - lines.append(row) - - lines.extend(["", "═" * 90, ""]) - - return "\n".join(lines) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# PYTEST TESTS -# ═══════════════════════════════════════════════════════════════════════════════ - - -@pytest.fixture -def single_evaluator(): - return ScenarioEvaluator(agent_name="single") - - -@pytest.fixture -def reflection_evaluator(): - return ScenarioEvaluator(agent_name="reflection") - - -class TestScenarioEvaluation: - """Scenario-based evaluation tests.""" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_single_scenario_billing(self, single_evaluator): - """Test single agent on billing scenario.""" - scenario = SCENARIOS[0] # billing_high_invoice - result = await single_evaluator.run_scenario(scenario) - - print(f"\nScenario: {scenario.name}") - print(f"Response: {result.query_results[0].response[:300]}...") - print(f"Tools called: {result.query_results[0].tool_calls}") - print(f"Tool F1: {result.tool_f1:.1%}") - print(f"Keyword coverage: {result.keyword_coverage:.1%}") - - assert result.keyword_coverage >= 0.3, "Should mention some relevant keywords" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_all_scenarios_single_agent(self, single_evaluator): - """Run all scenarios with single agent.""" - results = await single_evaluator.run_all_scenarios() - - passed = sum(1 for r in results if r.success) - print(f"\nSingle Agent: {passed}/{len(results)} scenarios passed") - - assert passed >= len(results) // 2, "At least half of scenarios should pass" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_all_scenarios_reflection_agent(self, reflection_evaluator): - """Run all scenarios with reflection agent.""" - results = await reflection_evaluator.run_all_scenarios() - - passed = sum(1 for r in results if r.success) - print(f"\nReflection Agent: {passed}/{len(results)} scenarios passed") - - assert passed >= len(results) // 2, "At least half of scenarios should pass" - - -class TestAgentComparison: - """Compare multiple agents on all scenarios.""" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_compare_single_vs_reflection(self): - """Compare single vs reflection agent with full metrics.""" - agents = ["single", "reflection"] - results_by_agent: dict[str, list[ScenarioResult]] = {} - - for agent_name in agents: - print(f"\n{'=' * 40}") - print(f"Running {agent_name} agent...") - print("=" * 40) - - evaluator = ScenarioEvaluator(agent_name=agent_name) - results = await evaluator.run_all_scenarios() - results_by_agent[agent_name] = results - - # Generate and print report - report = generate_comparison_report(results_by_agent) - print(report) - - # Save results - results_file = _eval_dir / "agent_comparison_results.json" - _save_results(results_by_agent, results_file) - - print(f"\nResults saved to: {results_file}") - - # Basic assertions - for agent_name, results in results_by_agent.items(): - passed = sum(1 for r in results if r.success) - assert passed >= 1, f"{agent_name} should pass at least 1 scenario" - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_compare_all_agents_parallel(self): - """Compare ALL agents in parallel for speed.""" - # Available agents: single, reflection, handoff, magentic - # Note: magentic excluded due to tool call tracking issues - agents = ["single", "reflection", "handoff"] - - async def run_agent(agent_name: str) -> tuple[str, list[ScenarioResult]]: - """Run single agent evaluation.""" - print(f"\n[{agent_name}] Starting evaluation...") - evaluator = ScenarioEvaluator(agent_name=agent_name) - results = await evaluator.run_all_scenarios() - passed = sum(1 for r in results if r.success) - print(f"[{agent_name}] Completed: {passed}/{len(results)} passed") - return agent_name, results - - # Run all agents in parallel - print("\n" + "=" * 60) - print("RUNNING ALL AGENTS IN PARALLEL") - print("=" * 60) - - import time - start_time = time.time() - - # Execute all agents concurrently - tasks = [run_agent(agent_name) for agent_name in agents] - agent_results = await asyncio.gather(*tasks, return_exceptions=True) - - total_time = time.time() - start_time - - # Collect results (filter out exceptions) - results_by_agent: dict[str, list[ScenarioResult]] = {} - for result in agent_results: - if isinstance(result, Exception): - print(f"Agent failed with error: {result}") - else: - agent_name, results = result - results_by_agent[agent_name] = results - - # Generate and print report - if results_by_agent: - report = generate_comparison_report(results_by_agent) - print(report) - - print(f"\nTotal parallel execution time: {total_time:.1f}s") - print(f"(Sequential would be ~{total_time * len(agents):.1f}s)") - - # Save results - results_file = _eval_dir / "all_agents_comparison.json" - _save_results(results_by_agent, results_file) - print(f"\nResults saved to: {results_file}") - - # Basic assertions - assert len(results_by_agent) >= 2, "At least 2 agents should complete" - for agent_name, results in results_by_agent.items(): - passed = sum(1 for r in results if r.success) - assert passed >= 1, f"{agent_name} should pass at least 1 scenario" - - -def _save_results(results_by_agent: dict[str, list[ScenarioResult]], results_file: Path): - """Save evaluation results to JSON file.""" - with open(results_file, "w") as f: - json.dump({ - agent_name: [ - { - "scenario": r.scenario.id, - "scenario_name": r.scenario.name, - "success": r.success, - # Rule-based metrics - "tool_recall": r.tool_recall, - "tool_precision": r.tool_precision, - "tool_f1": r.tool_f1, - "keyword_coverage": r.keyword_coverage, - "total_time": r.total_time, - "tools_called": [tc for qr in r.query_results for tc in qr.tool_calls], - # LLM-as-Judge metrics - "llm_intent_score": r.llm_intent_score, - "llm_intent_result": r.llm_intent_result, - "llm_intent_reason": r.llm_intent_reason, - "llm_task_score": r.llm_task_score, - "llm_task_result": r.llm_task_result, - "llm_task_reason": r.llm_task_reason, - "llm_tool_score": r.llm_tool_score, - "llm_coherence": r.llm_coherence, - "llm_fluency": r.llm_fluency, - "llm_relevance": r.llm_relevance, - "llm_solution_score": r.llm_solution_score, - "llm_solution_reason": r.llm_solution_reason, - "llm_eval_time": r.llm_eval_time, - } - for r in results - ] - for agent_name, results in results_by_agent.items() - }, f, indent=2) - - -# ═══════════════════════════════════════════════════════════════════════════════ -# STANDALONE EXECUTION -# ═══════════════════════════════════════════════════════════════════════════════ - -if __name__ == "__main__": - import logging - import warnings - - # Suppress MCP client cleanup warnings - logging.getLogger("asyncio").setLevel(logging.CRITICAL) - warnings.filterwarnings("ignore", category=DeprecationWarning) - - async def main(): - print("Agent Evaluation: Tool Accuracy + Outcome Quality") - print("=" * 60) - - agents = ["single", "reflection"] - results_by_agent: dict[str, list[ScenarioResult]] = {} - - for agent_name in agents: - print(f"\n{'─' * 40}") - print(f"Running {agent_name} agent on {len(SCENARIOS)} scenarios...") - print("─" * 40) - - evaluator = ScenarioEvaluator(agent_name=agent_name) - results = await evaluator.run_all_scenarios() - results_by_agent[agent_name] = results - - print(generate_comparison_report(results_by_agent)) - - asyncio.run(main()) diff --git a/tests/evaluation/uv.lock b/tests/evaluation/uv.lock deleted file mode 100644 index 412ec574a..000000000 --- a/tests/evaluation/uv.lock +++ /dev/null @@ -1,3198 +0,0 @@ -version = 1 -revision = 1 -requires-python = ">=3.12" -resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version < '3.13'", -] - -[options] -prerelease-mode = "allow" - -[[package]] -name = "a2a-sdk" -version = "0.3.22" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-api-core" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "protobuf" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/a3/76f2d94a32a1b0dc760432d893a09ec5ed31de5ad51b1ef0f9d199ceb260/a2a_sdk-0.3.22.tar.gz", hash = "sha256:77a5694bfc4f26679c11b70c7f1062522206d430b34bc1215cfbb1eba67b7e7d", size = 231535 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/e8/f4e39fd1cf0b3c4537b974637143f3ebfe1158dad7232d9eef15666a81ba/a2a_sdk-0.3.22-py3-none-any.whl", hash = "sha256:b98701135bb90b0ff85d35f31533b6b7a299bf810658c1c65f3814a6c15ea385", size = 144347 }, -] - -[[package]] -name = "ag-ui-protocol" -version = "0.1.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/bb/5a5ec893eea5805fb9a3db76a9888c3429710dfb6f24bbb37568f2cf7320/ag_ui_protocol-0.1.10.tar.gz", hash = "sha256:3213991c6b2eb24bb1a8c362ee270c16705a07a4c5962267a083d0959ed894f4", size = 6945 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889 }, -] - -[[package]] -name = "agent-evaluation" -version = "0.1.0" -source = { virtual = "." } -dependencies = [ - { name = "agent-framework" }, - { name = "azure-ai-evaluation" }, - { name = "azure-identity" }, - { name = "httpx" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-timeout" }, - { name = "python-dotenv" }, -] - -[package.metadata] -requires-dist = [ - { name = "agent-framework", specifier = "==1.0.0b260107" }, - { name = "azure-ai-evaluation", specifier = ">=1.0.0" }, - { name = "azure-identity", specifier = ">=1.15.0" }, - { name = "httpx", specifier = ">=0.27.0" }, - { name = "openai", specifier = ">=2.5.0" }, - { name = "pydantic", specifier = ">=2.0.0" }, - { name = "pytest", specifier = ">=8.0.0" }, - { name = "pytest-asyncio", specifier = ">=0.23.0" }, - { name = "pytest-timeout", specifier = ">=2.3.0" }, - { name = "python-dotenv", specifier = ">=1.0.0" }, -] - -[[package]] -name = "agent-framework" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core", extra = ["all"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7e/e7/5ad52075da4e586ca94fb8806b3085ac5dea8059413e413bff88c0452e88/agent_framework-1.0.0b260107.tar.gz", hash = "sha256:a2f6508a0ca1df3b7ca4e3a64e45bac8e33cdfe02cf69e9056e37e881a58aad7", size = 2898189 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/55/ffef27526cc26bf163ccf9d58ba87bf4e677bba343a542e7b666846f744d/agent_framework-1.0.0b260107-py3-none-any.whl", hash = "sha256:080deb32bff4ef07227a4ba709798c67079ff8a2997fe7a0aed0010adc0c18cf", size = 5554 }, -] - -[[package]] -name = "agent-framework-a2a" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "a2a-sdk" }, - { name = "agent-framework-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/04/9a/7314f4b4b9b3dffb0ace8681baf0e330a7fd8de55deb09f917024b854b3d/agent_framework_a2a-1.0.0b260107.tar.gz", hash = "sha256:f22f4eff856dd93d32ec07ffc30608ca54308c4fdcc007c028d8616314893b46", size = 7281 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/92/45e37c57427b9613e54fc3ad865cfc4d4784a0287576b55fd6f3f884056b/agent_framework_a2a-1.0.0b260107-py3-none-any.whl", hash = "sha256:e56f9836c6fb5d60b0750a8a1339f0f09cec6e3ea2ef3bf327ea5c10378b7dff", size = 7502 }, -] - -[[package]] -name = "agent-framework-ag-ui" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ag-ui-protocol" }, - { name = "agent-framework-core" }, - { name = "fastapi" }, - { name = "uvicorn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a2/d5/11fe7cae81192d0ffe816c59ddf0284b947a7a32da3072c99f2bb11e9a5c/agent_framework_ag_ui-1.0.0b260107.tar.gz", hash = "sha256:c0f79f08c3ea2c1a6454fab8cd46a5f94df2e8db71a76b5d7906735087f66349", size = 85637 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/5b/3675630c6ed72213c2309c1b6b92a7b9496e42ca249826625c8cb4e16796/agent_framework_ag_ui-1.0.0b260107-py3-none-any.whl", hash = "sha256:532a34ebbb761cf5511db4ac6b1c5461cf0ee266bf0ccd961f4f8fb9ca5dff5f", size = 62472 }, -] - -[[package]] -name = "agent-framework-anthropic" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "anthropic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1a/d4/9d002f6333f45d453fc8766b73df0d9fb69e486c678abea017215949e66d/agent_framework_anthropic-1.0.0b260107.tar.gz", hash = "sha256:731d8d16e4a39030e382ae826f0fd123b04a64c4020435ad0ba6290bd461b2f3", size = 9321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/75/daaabe378802a918d7bceb6c52e04b332112c89c819f9eaaa00f1f1f37b0/agent_framework_anthropic-1.0.0b260107-py3-none-any.whl", hash = "sha256:47a4fe893769a15594c663ae2f27132f32cea4393bffe4578a1df49ee70f8a23", size = 9322 }, -] - -[[package]] -name = "agent-framework-azure-ai" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "aiohttp" }, - { name = "azure-ai-agents" }, - { name = "azure-ai-projects" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/26/954d48dbe6e2d558e8425dce1a62238787350f501443081f0de9eab0d9c5/agent_framework_azure_ai-1.0.0b260107.tar.gz", hash = "sha256:bfbec64bf89382833aea18526bb4970b540f9afb269a0eb96bbaed07a3ae6f66", size = 19840 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/c0/ca16e4d772baa2b9d94efebefb5c0d795cddc1428b25a40f4eee7eec8415/agent_framework_azure_ai-1.0.0b260107-py3-none-any.whl", hash = "sha256:001f82bec04d73a8d5e0cf34a9f613963e50db7d46ae000625554306c8271976", size = 21431 }, -] - -[[package]] -name = "agent-framework-azure-ai-search" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "azure-search-documents" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/e6/15f6bb752e900a4262bc2469c3947d7bd85793ebe88b596fa7ea11c0eec5/agent_framework_azure_ai_search-1.0.0b260107.tar.gz", hash = "sha256:1037e1addcab8805f000b0a24725470715fcd758b2a165650a28583dcd30d1b1", size = 13317 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/c9/81379dca1f280222170d6561d63f5ed1f0e2477e51926f081d4e7cd2bb88/agent_framework_azure_ai_search-1.0.0b260107-py3-none-any.whl", hash = "sha256:59dd3e559ca2920b952c4786b4889e060fa7b0f4df1e236c43a82e92142aaa86", size = 13447 }, -] - -[[package]] -name = "agent-framework-azurefunctions" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "azure-functions" }, - { name = "azure-functions-durable" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/74/94a8e1aa0f4264f75c992d76f61fc13f73ba28ecfaabebb132b76a77aa9c/agent_framework_azurefunctions-1.0.0b260107.tar.gz", hash = "sha256:83c22ecd1706593e5223cafd0c348a4cf2d3379d8d06528940e2d77cb66c752e", size = 33705 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/b7/e0ac2145d7c7dadca7c7cae03d31f097e9b913c132311fc5e781efe351a4/agent_framework_azurefunctions-1.0.0b260107-py3-none-any.whl", hash = "sha256:97581152a4d4e7a9dad1199e5d748bb77ef63522572d5c6cb9de4717372b2037", size = 37356 }, -] - -[[package]] -name = "agent-framework-chatkit" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "openai-chatkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/8a/c0d1afda3707f9a369be8a235a493ce6c3a645fe87b9ce414dbac97373cd/agent_framework_chatkit-1.0.0b260107.tar.gz", hash = "sha256:9bd46fe9f22acb741c75bde038d738489a518c30dad56b16ad26592598e870f5", size = 12428 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/cd/d7e578239a89977028584dfc8494901cb83824a0f1045369ed55f1dd9c7d/agent_framework_chatkit-1.0.0b260107-py3-none-any.whl", hash = "sha256:88665fd24bafb78b8649d10d267dd27f62cac0b70489044299574288ba8457f3", size = 11726 }, -] - -[[package]] -name = "agent-framework-copilotstudio" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "microsoft-agents-copilotstudio-client" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/41/e7/43d3f8b4650b4c4ff214a6340676b7d3bd8087ba280fbbfedc91746bcabf/agent_framework_copilotstudio-1.0.0b260107.tar.gz", hash = "sha256:72d53bd625540786c0989c78e3f57a5941349ec2dc0dfc74c4bd85e0c4e79b47", size = 8525 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/3c/2fbe13fbcc97a4568d34604f1a730af2699b201c50627a918aa02951a680/agent_framework_copilotstudio-1.0.0b260107-py3-none-any.whl", hash = "sha256:dbd5bf97460de6f40cac524f52acd458cb1a1c6c1cac1c8bb3317edf0112fd90", size = 8711 }, -] - -[[package]] -name = "agent-framework-core" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-identity" }, - { name = "mcp", extra = ["ws"] }, - { name = "openai" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "opentelemetry-semantic-conventions-ai" }, - { name = "packaging" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/44/06f5d2c99dd7bdb82c2cb5cbc354b5bc6af72d1886d20eff1dff83508fae/agent_framework_core-1.0.0b260107.tar.gz", hash = "sha256:12636fb64664c6153546f0d85dafccdbe57226767c14b3f38985867389f980bb", size = 3574757 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5a/8c6315a2ca119ad48340344616d4b8e77fd68e2892f82c402069a52ad647/agent_framework_core-1.0.0b260107-py3-none-any.whl", hash = "sha256:5bd119b8d30dc2d5bee1c4a5c3597d7afc808a52e4de148725c4f2d9bcc7632b", size = 5687298 }, -] - -[package.optional-dependencies] -all = [ - { name = "agent-framework-a2a" }, - { name = "agent-framework-ag-ui" }, - { name = "agent-framework-anthropic" }, - { name = "agent-framework-azure-ai" }, - { name = "agent-framework-azure-ai-search" }, - { name = "agent-framework-azurefunctions" }, - { name = "agent-framework-chatkit" }, - { name = "agent-framework-copilotstudio" }, - { name = "agent-framework-declarative" }, - { name = "agent-framework-devui" }, - { name = "agent-framework-lab" }, - { name = "agent-framework-mem0" }, - { name = "agent-framework-ollama" }, - { name = "agent-framework-purview" }, - { name = "agent-framework-redis" }, -] - -[[package]] -name = "agent-framework-declarative" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "powerfx", marker = "python_full_version < '3.14'" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/48/30/22fb13d4ae2a13a138ad245fcfbe9aa38f5b7dbdc0cd9672fd6db874ee92/agent_framework_declarative-1.0.0b260107.tar.gz", hash = "sha256:8edf62c8cae0c67e4cbdb713c0e35c4ceaf7ccabb6f1a2b950d4b8796e29bc84", size = 12757 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/20/0c/4db67ac51cfad217f1928e3f64ab512ca34e2a7b8d0dfe9e09c6fadecf80/agent_framework_declarative-1.0.0b260107-py3-none-any.whl", hash = "sha256:35004053cbfd0217cf802467d87f51324822be351dd67f5e12f9b851019bb5b0", size = 13510 }, -] - -[[package]] -name = "agent-framework-devui" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "fastapi" }, - { name = "python-dotenv" }, - { name = "uvicorn", extra = ["standard"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/cc/144aec868a2d599e5a779a3f17fb98c77ea4e9e8bd909c559981bc789252/agent_framework_devui-1.0.0b260107.tar.gz", hash = "sha256:af025563bd5e7ec626027610fb43553e33a741487465bc9abbcdf11f751860bb", size = 356007 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/b9/c7b6c12b4e0bfd6f4d4671512cd5805477abf9d1d93201786d24d969bcf2/agent_framework_devui-1.0.0b260107-py3-none-any.whl", hash = "sha256:94039e7a0a0cddf343ee40fd3209bb16b9343c33fcbe288a1b31da19cd991260", size = 361044 }, -] - -[[package]] -name = "agent-framework-lab" -version = "1.0.0b251024" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/c5/be86273cb3545651d0c8112ff9f38ae8fe13b740ce9b65b9be83ff2d70ee/agent_framework_lab-1.0.0b251024.tar.gz", hash = "sha256:4261cb595b6edfd4f30db613c1885c71b3dcfa2088cf29224d4f17b3ff956b2a", size = 23397 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/0f/3974b2b1f6bf523ee3ced0886b6afd5ca8bbebd24aa5278ef77db0d3d765/agent_framework_lab-1.0.0b251024-py3-none-any.whl", hash = "sha256:1596408991a92fcacef4bb939305d2b59159517b707f48114105fc0dd46bfee7", size = 26589 }, -] - -[[package]] -name = "agent-framework-mem0" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "mem0ai" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/aa/49/8c000c562a0bfc2cdf160253a030fbc21771db69c009f9970902e1ddd65b/agent_framework_mem0-1.0.0b260107.tar.gz", hash = "sha256:11c9672e2cd7f2f74213472fd4abed26a913fa6443f9224804f3c9b1b58f74b7", size = 5400 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/07/fae73c5b0045dc78c685348371402a6dbadd83147da744a44ade7d7ad06d/agent_framework_mem0-1.0.0b260107-py3-none-any.whl", hash = "sha256:c52751565da07524bf2317fdd75068bdd03c73b7002d82acee393821485909e6", size = 5573 }, -] - -[[package]] -name = "agent-framework-ollama" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "ollama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/ba/23eaba3ea5220f1752d8d4a398a41951c7f7b1fc650cf1fed48c7e4e5127/agent_framework_ollama-1.0.0b260107.tar.gz", hash = "sha256:412c098eedb170d76e15eadc5b0bc9f5792a7e13d655cb1e7f03e8e9fb4d6950", size = 5982 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/30/f821646487fb08018c240ca1ecbb5c4684378dfb48c192b6c1bf778dc286/agent_framework_ollama-1.0.0b260107-py3-none-any.whl", hash = "sha256:11c46a8495f58a71044c648476ff982fede1ad1e64cda28c9a9128ca3674d7b0", size = 7029 }, -] - -[[package]] -name = "agent-framework-purview" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "azure-core" }, - { name = "httpx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/31/e7/097789fad41cdc4c477a78278a25e9af0e35c328dee612ad46bdbdda3e15/agent_framework_purview-1.0.0b260107.tar.gz", hash = "sha256:f12fb52b1d4ce0dc593458182ac901dafaf1bdcca9a86aa7cfe16f27546bcf89", size = 26814 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/43/04a107ae1d46a53c4f9423a87e75c352d4810665d6a8c0b2d28f06f92360/agent_framework_purview-1.0.0b260107-py3-none-any.whl", hash = "sha256:74d39279a84333a7e343fec2e2b4723700b58e2bdb3d18a315af3a03efd77018", size = 26176 }, -] - -[[package]] -name = "agent-framework-redis" -version = "1.0.0b260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "agent-framework-core" }, - { name = "numpy" }, - { name = "redis" }, - { name = "redisvl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/9c/57332b52089240adba1fae311893bc003238434ddb31773e82d32a64b4b1/agent_framework_redis-1.0.0b260107.tar.gz", hash = "sha256:a66fb64646521967995ee0ea0970695c66d016838f3f8f965e0c21a406f48c41", size = 15714 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/57/6e/1aa99fc437481370f5256c23a29ff9899dd6e727af8b928fb06620b339a6/agent_framework_redis-1.0.0b260107-py3-none-any.whl", hash = "sha256:77a4276ece6c28ed65a53a1b399132fe2920f8da9bbd83eb87efb1eb41c44118", size = 16051 }, -] - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265 }, -] - -[[package]] -name = "aiohttp" -version = "3.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohappyeyeballs" }, - { name = "aiosignal" }, - { name = "attrs" }, - { name = "frozenlist" }, - { name = "multidict" }, - { name = "propcache" }, - { name = "yarl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/be/4fc11f202955a69e0db803a12a062b8379c970c7c84f4882b6da17337cc1/aiohttp-3.13.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c", size = 739732 }, - { url = "https://files.pythonhosted.org/packages/97/2c/621d5b851f94fa0bb7430d6089b3aa970a9d9b75196bc93bb624b0db237a/aiohttp-3.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168", size = 494293 }, - { url = "https://files.pythonhosted.org/packages/5d/43/4be01406b78e1be8320bb8316dc9c42dbab553d281c40364e0f862d5661c/aiohttp-3.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d", size = 493533 }, - { url = "https://files.pythonhosted.org/packages/8d/a8/5a35dc56a06a2c90d4742cbf35294396907027f80eea696637945a106f25/aiohttp-3.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29", size = 1737839 }, - { url = "https://files.pythonhosted.org/packages/bf/62/4b9eeb331da56530bf2e198a297e5303e1c1ebdceeb00fe9b568a65c5a0c/aiohttp-3.13.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3", size = 1703932 }, - { url = "https://files.pythonhosted.org/packages/7c/f6/af16887b5d419e6a367095994c0b1332d154f647e7dc2bd50e61876e8e3d/aiohttp-3.13.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d", size = 1771906 }, - { url = "https://files.pythonhosted.org/packages/ce/83/397c634b1bcc24292fa1e0c7822800f9f6569e32934bdeef09dae7992dfb/aiohttp-3.13.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463", size = 1871020 }, - { url = "https://files.pythonhosted.org/packages/86/f6/a62cbbf13f0ac80a70f71b1672feba90fdb21fd7abd8dbf25c0105fb6fa3/aiohttp-3.13.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc", size = 1755181 }, - { url = "https://files.pythonhosted.org/packages/0a/87/20a35ad487efdd3fba93d5843efdfaa62d2f1479eaafa7453398a44faf13/aiohttp-3.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf", size = 1561794 }, - { url = "https://files.pythonhosted.org/packages/de/95/8fd69a66682012f6716e1bc09ef8a1a2a91922c5725cb904689f112309c4/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033", size = 1697900 }, - { url = "https://files.pythonhosted.org/packages/e5/66/7b94b3b5ba70e955ff597672dad1691333080e37f50280178967aff68657/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f", size = 1728239 }, - { url = "https://files.pythonhosted.org/packages/47/71/6f72f77f9f7d74719692ab65a2a0252584bf8d5f301e2ecb4c0da734530a/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679", size = 1740527 }, - { url = "https://files.pythonhosted.org/packages/fa/b4/75ec16cbbd5c01bdaf4a05b19e103e78d7ce1ef7c80867eb0ace42ff4488/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423", size = 1554489 }, - { url = "https://files.pythonhosted.org/packages/52/8f/bc518c0eea29f8406dcf7ed1f96c9b48e3bc3995a96159b3fc11f9e08321/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce", size = 1767852 }, - { url = "https://files.pythonhosted.org/packages/9d/f2/a07a75173124f31f11ea6f863dc44e6f09afe2bca45dd4e64979490deab1/aiohttp-3.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a", size = 1722379 }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253 }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407 }, - { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190 }, - { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783 }, - { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704 }, - { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652 }, - { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014 }, - { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777 }, - { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276 }, - { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131 }, - { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863 }, - { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793 }, - { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676 }, - { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217 }, - { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303 }, - { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673 }, - { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120 }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383 }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899 }, - { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238 }, - { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292 }, - { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021 }, - { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263 }, - { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107 }, - { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196 }, - { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591 }, - { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277 }, - { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575 }, - { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455 }, - { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417 }, - { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968 }, - { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690 }, - { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390 }, - { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188 }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126 }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128 }, - { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512 }, - { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444 }, - { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798 }, - { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835 }, - { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486 }, - { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951 }, - { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001 }, - { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246 }, - { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131 }, - { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196 }, - { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841 }, - { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193 }, - { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979 }, - { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193 }, - { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801 }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523 }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694 }, -] - -[[package]] -name = "aiosignal" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "frozenlist" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, -] - -[[package]] -name = "annotated-doc" -version = "0.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303 }, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anthropic" -version = "0.76.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "docstring-parser" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6e/be/d11abafaa15d6304826438170f7574d750218f49a106c54424a40cef4494/anthropic-0.76.0.tar.gz", hash = "sha256:e0cae6a368986d5cf6df743dfbb1b9519e6a9eee9c6c942ad8121c0b34416ffe", size = 495483 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/70/7b0fd9c1a738f59d3babe2b4212031c34ab7d0fda4ffef15b58a55c5bcea/anthropic-0.76.0-py3-none-any.whl", hash = "sha256:81efa3113901192af2f0fe977d3ec73fdadb1e691586306c4256cd6d5ccc331c", size = 390309 }, -] - -[[package]] -name = "anyio" -version = "4.12.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592 }, -] - -[[package]] -name = "attrs" -version = "25.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/5c/685e6633917e101e5dcb62b9dd76946cbb57c26e133bae9e0cd36033c0a9/attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11", size = 934251 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615 }, -] - -[[package]] -name = "azure-ai-agents" -version = "1.2.0b5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/57/8adeed578fa8984856c67b4229e93a58e3f6024417d448d0037aafa4ee9b/azure_ai_agents-1.2.0b5.tar.gz", hash = "sha256:1a16ef3f305898aac552269f01536c34a00473dedee0bca731a21fdb739ff9d5", size = 394876 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/6d/15070d23d7a94833a210da09d5d7ed3c24838bb84f0463895e5d159f1695/azure_ai_agents-1.2.0b5-py3-none-any.whl", hash = "sha256:257d0d24a6bf13eed4819cfa5c12fb222e5908deafb3cbfd5711d3a511cc4e88", size = 217948 }, -] - -[[package]] -name = "azure-ai-evaluation" -version = "1.14.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "azure-core" }, - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "httpx" }, - { name = "jinja2" }, - { name = "msrest" }, - { name = "nltk" }, - { name = "openai" }, - { name = "pandas" }, - { name = "pyjwt" }, - { name = "ruamel-yaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/ab/62300008df848b210ef2a21b646480eee7c1bf3906afdc1351795343321c/azure_ai_evaluation-1.14.0.tar.gz", hash = "sha256:2a5681805b7cde65ad663f34d0f647d28498dd9395f7e2ce0789320c26664dae", size = 2196726 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/68/1e0bd2123a5e681dbe474a3dda098c85704556e53ae24c7f4b3915d4e048/azure_ai_evaluation-1.14.0-py3-none-any.whl", hash = "sha256:1785f9be28517839ab9d30a03893951f7c9b530500d939d0ae51dde3aa1478b0", size = 1141136 }, -] - -[[package]] -name = "azure-ai-projects" -version = "2.0.0b3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "azure-identity" }, - { name = "azure-storage-blob" }, - { name = "isodate" }, - { name = "openai" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/24/e0/3512d3f07e9dd2eb4af684387c31598c435bd87833b6a81850972963cb9c/azure_ai_projects-2.0.0b3.tar.gz", hash = "sha256:6d09ad110086e450a47b991ee8a3644f1be97fa3085d5981d543f900d78f4505", size = 431749 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/b6/8fbd4786bb5c0dd19eaff86ddce0fbfb53a6f90d712038272161067a076a/azure_ai_projects-2.0.0b3-py3-none-any.whl", hash = "sha256:3b3048a3ba3904d556ba392b7bd20b6e84c93bb39df6d43a6470cdb0ad08af8c", size = 240717 }, -] - -[[package]] -name = "azure-common" -version = "1.1.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462 }, -] - -[[package]] -name = "azure-core" -version = "1.38.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/1b/e503e08e755ea94e7d3419c9242315f888fc664211c90d032e40479022bf/azure_core-1.38.0.tar.gz", hash = "sha256:8194d2682245a3e4e3151a667c686464c3786fed7918b394d035bdcd61bb5993", size = 363033 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/d8/b8fcba9464f02b121f39de2db2bf57f0b216fe11d014513d666e8634380d/azure_core-1.38.0-py3-none-any.whl", hash = "sha256:ab0c9b2cd71fecb1842d52c965c95285d3cfb38902f6766e4a471f1cd8905335", size = 217825 }, -] - -[[package]] -name = "azure-functions" -version = "1.25.0b3.dev1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/a3/8d6d1f3d7869363028a2488e6b3fed7375be0c652933a6b701dbe8ebff36/azure_functions-1.25.0b3.dev1.tar.gz", hash = "sha256:f9777661b0fd14e6a6ad7a85bb179ba59c80ffa64ec15f1728848154c9135c2e", size = 142121 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/3f/d3a446d76159cb1e2015e7a24b888d2affc28d68c59795252133e6474cad/azure_functions-1.25.0b3.dev1-py3-none-any.whl", hash = "sha256:3ba27c26310c112d0955e1dae19fa378b40b509ff1c59e1a45826a28042d21a3", size = 114184 }, -] - -[[package]] -name = "azure-functions-durable" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "aiohttp" }, - { name = "azure-functions" }, - { name = "furl" }, - { name = "opentelemetry-api" }, - { name = "opentelemetry-sdk" }, - { name = "python-dateutil" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/51/3a/f168b434fa69eaaf5d14b54d88239b851eceb7e10f666b55289dd0933ccb/azure-functions-durable-1.4.0.tar.gz", hash = "sha256:945488ef28917dae4295a4dd6e6f6601ffabe32e3fbb94ceb261c9b65b6e6c0f", size = 176584 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/01/7f03229fa5c05a5cc7e41172aef80c5242d28aeea0825f592f93141a4b91/azure_functions_durable-1.4.0-py3-none-any.whl", hash = "sha256:0efe919cdda96924791feabe192a37c7d872414b4c6ce348417a02ee53d8cc31", size = 143159 }, -] - -[[package]] -name = "azure-identity" -version = "1.26.0b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "msal" }, - { name = "msal-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/b0/0c93d0d35694d5015f565a70ef5428ba640a3ba3bc082e24be4d72a3a915/azure_identity-1.26.0b1.tar.gz", hash = "sha256:401197087ec14ee29cfbfcd099453d56037bef252954fee04b5d26ccb702c869", size = 292298 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/28/af9ef022f21e3b51b3718d4348f771b490678c1116563895547c0a771362/azure_identity-1.26.0b1-py3-none-any.whl", hash = "sha256:dc608b59ae628a38611208ee761adeb1a2b9390258b58d6edcda2d24c50a4348", size = 197227 }, -] - -[[package]] -name = "azure-search-documents" -version = "11.7.0b2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-common" }, - { name = "azure-core" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/ba/bde0f03e0a742ba3bbcc929f91ed2f3b1420c2bb84c9a7f878f3b87ebfce/azure_search_documents-11.7.0b2.tar.gz", hash = "sha256:b6e039f8038ff2210d2057e704e867c6e29bb46bfcd400da4383e45e4b8bb189", size = 423956 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/26/ed4498374f9088818278ac225f2bea688b4ec979d81bf83a5355c8c366af/azure_search_documents-11.7.0b2-py3-none-any.whl", hash = "sha256:f82117b321344a84474269ed26df194c24cca619adc024d981b1b86aee3c6f05", size = 432037 }, -] - -[[package]] -name = "azure-storage-blob" -version = "12.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "cryptography" }, - { name = "isodate" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/24/072ba8e27b0e2d8fec401e9969b429d4f5fc4c8d4f0f05f4661e11f7234a/azure_storage_blob-12.28.0.tar.gz", hash = "sha256:e7d98ea108258d29aa0efbfd591b2e2075fa1722a2fae8699f0b3c9de11eff41", size = 604225 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/3a/6ef2047a072e54e1142718d433d50e9514c999a58f51abfff7902f3a72f8/azure_storage_blob-12.28.0-py3-none-any.whl", hash = "sha256:00fb1db28bf6a7b7ecaa48e3b1d5c83bfadacc5a678b77826081304bd87d6461", size = 431499 }, -] - -[[package]] -name = "backoff" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148 }, -] - -[[package]] -name = "certifi" -version = "2026.1.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900 }, -] - -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048 }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529 }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097 }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983 }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519 }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572 }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963 }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361 }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230 }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043 }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446 }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101 }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948 }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422 }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499 }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928 }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302 }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909 }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402 }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780 }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320 }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487 }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049 }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793 }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300 }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244 }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828 }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926 }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328 }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650 }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687 }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773 }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013 }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593 }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354 }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480 }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584 }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443 }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437 }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487 }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726 }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, - { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, - { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, - { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, - { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, - { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, - { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, - { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, - { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, - { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, - { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, - { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, - { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, - { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, - { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, - { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, -] - -[[package]] -name = "click" -version = "8.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274 }, -] - -[[package]] -name = "clr-loader" -version = "0.2.10" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "python_full_version < '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/24/c12faf3f61614b3131b5c98d3bf0d376b49c7feaa73edca559aeb2aee080/clr_loader-0.2.10.tar.gz", hash = "sha256:81f114afbc5005bafc5efe5af1341d400e22137e275b042a8979f3feb9fc9446", size = 83605 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/61/cf819f8e8bb4d4c74661acf2498ba8d4a296714be3478d21eaabf64f5b9b/clr_loader-0.2.10-py3-none-any.whl", hash = "sha256:ebbbf9d511a7fe95fa28a95a4e04cd195b097881dfe66158dc2c281d3536f282", size = 56483 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "cryptography" -version = "46.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004 }, - { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667 }, - { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807 }, - { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615 }, - { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800 }, - { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707 }, - { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541 }, - { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464 }, - { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838 }, - { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596 }, - { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782 }, - { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381 }, - { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988 }, - { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451 }, - { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007 }, - { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012 }, - { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728 }, - { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078 }, - { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460 }, - { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237 }, - { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344 }, - { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564 }, - { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415 }, - { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457 }, - { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074 }, - { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569 }, - { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941 }, - { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339 }, - { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315 }, - { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331 }, - { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248 }, - { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089 }, - { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029 }, - { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222 }, - { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280 }, - { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958 }, - { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714 }, - { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970 }, - { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236 }, - { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642 }, - { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126 }, - { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573 }, - { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695 }, - { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720 }, - { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740 }, -] - -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277 }, -] - -[[package]] -name = "docstring-parser" -version = "0.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896 }, -] - -[[package]] -name = "fastapi" -version = "0.128.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-doc" }, - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094 }, -] - -[[package]] -name = "frozenlist" -version = "1.8.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782 }, - { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594 }, - { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448 }, - { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411 }, - { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014 }, - { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909 }, - { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049 }, - { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485 }, - { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619 }, - { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320 }, - { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820 }, - { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518 }, - { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096 }, - { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985 }, - { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591 }, - { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102 }, - { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717 }, - { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651 }, - { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417 }, - { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391 }, - { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048 }, - { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549 }, - { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833 }, - { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363 }, - { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314 }, - { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365 }, - { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763 }, - { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110 }, - { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717 }, - { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628 }, - { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882 }, - { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676 }, - { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235 }, - { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742 }, - { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725 }, - { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533 }, - { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506 }, - { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161 }, - { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676 }, - { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638 }, - { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067 }, - { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101 }, - { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901 }, - { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395 }, - { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659 }, - { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492 }, - { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034 }, - { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749 }, - { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127 }, - { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698 }, - { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749 }, - { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298 }, - { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015 }, - { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038 }, - { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130 }, - { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845 }, - { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131 }, - { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542 }, - { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308 }, - { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210 }, - { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972 }, - { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536 }, - { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330 }, - { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627 }, - { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238 }, - { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738 }, - { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739 }, - { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186 }, - { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196 }, - { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830 }, - { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289 }, - { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318 }, - { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814 }, - { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762 }, - { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470 }, - { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042 }, - { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148 }, - { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676 }, - { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451 }, - { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507 }, - { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409 }, -] - -[[package]] -name = "furl" -version = "2.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "orderedmultidict" }, - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/53/e4/203a76fa2ef46cdb0a618295cc115220cbb874229d4d8721068335eb87f0/furl-2.1.4.tar.gz", hash = "sha256:877657501266c929269739fb5f5980534a41abd6bbabcb367c136d1d3b2a6015", size = 57526 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/8c/dce3b1b7593858eba995b2dfdb833f872c7f863e3da92aab7128a6b11af4/furl-2.1.4-py2.py3-none-any.whl", hash = "sha256:da34d0b34e53ffe2d2e6851a7085a05d96922b5b578620a37377ff1dbeeb11c8", size = 27550 }, -] - -[[package]] -name = "google-api-core" -version = "2.29.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "google-auth" }, - { name = "googleapis-common-protos" }, - { name = "proto-plus" }, - { name = "protobuf" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/10/05572d33273292bac49c2d1785925f7bc3ff2fe50e3044cf1062c1dde32e/google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7", size = 177828 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/77/b6/85c4d21067220b9a78cfb81f516f9725ea6befc1544ec9bd2c1acd97c324/google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9", size = 173906 }, -] - -[[package]] -name = "google-auth" -version = "2.47.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1-modules" }, - { name = "rsa" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/60/3c/ec64b9a275ca22fa1cd3b6e77fefcf837b0732c890aa32d2bd21313d9b33/google_auth-2.47.0.tar.gz", hash = "sha256:833229070a9dfee1a353ae9877dcd2dec069a8281a4e72e72f77d4a70ff945da", size = 323719 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/db/18/79e9008530b79527e0d5f79e7eef08d3b179b7f851cfd3a2f27822fbdfa9/google_auth-2.47.0-py3-none-any.whl", hash = "sha256:c516d68336bfde7cf0da26aab674a36fedcf04b37ac4edd59c597178760c3498", size = 234867 }, -] - -[[package]] -name = "googleapis-common-protos" -version = "1.72.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515 }, -] - -[[package]] -name = "greenlet" -version = "3.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/e5/40dbda2736893e3e53d25838e0f19a2b417dfc122b9989c91918db30b5d3/greenlet-3.3.0.tar.gz", hash = "sha256:a82bb225a4e9e4d653dd2fb7b8b2d36e4fb25bc0165422a11e48b88e9e6f78fb", size = 190651 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379 }, - { url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294 }, - { url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742 }, - { url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297 }, - { url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885 }, - { url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424 }, - { url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017 }, - { url = "https://files.pythonhosted.org/packages/6c/79/3912a94cf27ec503e51ba493692d6db1e3cd8ac7ac52b0b47c8e33d7f4f9/greenlet-3.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7a34b13d43a6b78abf828a6d0e87d3385680eaf830cd60d20d52f249faabf39", size = 301964 }, - { url = "https://files.pythonhosted.org/packages/02/2f/28592176381b9ab2cafa12829ba7b472d177f3acc35d8fbcf3673d966fff/greenlet-3.3.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:a1e41a81c7e2825822f4e068c48cb2196002362619e2d70b148f20a831c00739", size = 275140 }, - { url = "https://files.pythonhosted.org/packages/2c/80/fbe937bf81e9fca98c981fe499e59a3f45df2a04da0baa5c2be0dca0d329/greenlet-3.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f515a47d02da4d30caaa85b69474cec77b7929b2e936ff7fb853d42f4bf8808", size = 599219 }, - { url = "https://files.pythonhosted.org/packages/c2/ff/7c985128f0514271b8268476af89aee6866df5eec04ac17dcfbc676213df/greenlet-3.3.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2d9fd66bfadf230b385fdc90426fcd6eb64db54b40c495b72ac0feb5766c54", size = 610211 }, - { url = "https://files.pythonhosted.org/packages/79/07/c47a82d881319ec18a4510bb30463ed6891f2ad2c1901ed5ec23d3de351f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30a6e28487a790417d036088b3bcb3f3ac7d8babaa7d0139edbaddebf3af9492", size = 624311 }, - { url = "https://files.pythonhosted.org/packages/fd/8e/424b8c6e78bd9837d14ff7df01a9829fc883ba2ab4ea787d4f848435f23f/greenlet-3.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:087ea5e004437321508a8d6f20efc4cfec5e3c30118e1417ea96ed1d93950527", size = 612833 }, - { url = "https://files.pythonhosted.org/packages/b5/ba/56699ff9b7c76ca12f1cdc27a886d0f81f2189c3455ff9f65246780f713d/greenlet-3.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ab97cf74045343f6c60a39913fa59710e4bd26a536ce7ab2397adf8b27e67c39", size = 1567256 }, - { url = "https://files.pythonhosted.org/packages/1e/37/f31136132967982d698c71a281a8901daf1a8fbab935dce7c0cf15f942cc/greenlet-3.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5375d2e23184629112ca1ea89a53389dddbffcf417dad40125713d88eb5f96e8", size = 1636483 }, - { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833 }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f0a6d0ede2c7bf092d00bc83ad5bafb7e6ec9b4aab2fbdfa6f134dc73327/greenlet-3.3.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:60c2ef0f578afb3c8d92ea07ad327f9a062547137afe91f38408f08aacab667f", size = 275671 }, - { url = "https://files.pythonhosted.org/packages/44/06/dac639ae1a50f5969d82d2e3dd9767d30d6dbdbab0e1a54010c8fe90263c/greenlet-3.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a5d554d0712ba1de0a6c94c640f7aeba3f85b3a6e1f2899c11c2c0428da9365", size = 646360 }, - { url = "https://files.pythonhosted.org/packages/e0/94/0fb76fe6c5369fba9bf98529ada6f4c3a1adf19e406a47332245ef0eb357/greenlet-3.3.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3a898b1e9c5f7307ebbde4102908e6cbfcb9ea16284a3abe15cab996bee8b9b3", size = 658160 }, - { url = "https://files.pythonhosted.org/packages/93/79/d2c70cae6e823fac36c3bbc9077962105052b7ef81db2f01ec3b9bf17e2b/greenlet-3.3.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dcd2bdbd444ff340e8d6bdf54d2f206ccddbb3ccfdcd3c25bf4afaa7b8f0cf45", size = 671388 }, - { url = "https://files.pythonhosted.org/packages/b8/14/bab308fc2c1b5228c3224ec2bf928ce2e4d21d8046c161e44a2012b5203e/greenlet-3.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5773edda4dc00e173820722711d043799d3adb4f01731f40619e07ea2750b955", size = 660166 }, - { url = "https://files.pythonhosted.org/packages/4b/d2/91465d39164eaa0085177f61983d80ffe746c5a1860f009811d498e7259c/greenlet-3.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac0549373982b36d5fd5d30beb8a7a33ee541ff98d2b502714a09f1169f31b55", size = 1615193 }, - { url = "https://files.pythonhosted.org/packages/42/1b/83d110a37044b92423084d52d5d5a3b3a73cafb51b547e6d7366ff62eff1/greenlet-3.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d198d2d977460358c3b3a4dc844f875d1adb33817f0613f663a656f463764ccc", size = 1683653 }, - { url = "https://files.pythonhosted.org/packages/7c/9a/9030e6f9aa8fd7808e9c31ba4c38f87c4f8ec324ee67431d181fe396d705/greenlet-3.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:73f51dd0e0bdb596fb0417e475fa3c5e32d4c83638296e560086b8d7da7c4170", size = 305387 }, - { url = "https://files.pythonhosted.org/packages/a0/66/bd6317bc5932accf351fc19f177ffba53712a202f9df10587da8df257c7e/greenlet-3.3.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:d6ed6f85fae6cdfdb9ce04c9bf7a08d666cfcfb914e7d006f44f840b46741931", size = 282638 }, - { url = "https://files.pythonhosted.org/packages/30/cf/cc81cb030b40e738d6e69502ccbd0dd1bced0588e958f9e757945de24404/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9125050fcf24554e69c4cacb086b87b3b55dc395a8b3ebe6487b045b2614388", size = 651145 }, - { url = "https://files.pythonhosted.org/packages/9c/ea/1020037b5ecfe95ca7df8d8549959baceb8186031da83d5ecceff8b08cd2/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:87e63ccfa13c0a0f6234ed0add552af24cc67dd886731f2261e46e241608bee3", size = 654236 }, - { url = "https://files.pythonhosted.org/packages/69/cc/1e4bae2e45ca2fa55299f4e85854606a78ecc37fead20d69322f96000504/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2662433acbca297c9153a4023fe2161c8dcfdcc91f10433171cf7e7d94ba2221", size = 662506 }, - { url = "https://files.pythonhosted.org/packages/57/b9/f8025d71a6085c441a7eaff0fd928bbb275a6633773667023d19179fe815/greenlet-3.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3c6e9b9c1527a78520357de498b0e709fb9e2f49c3a513afd5a249007261911b", size = 653783 }, - { url = "https://files.pythonhosted.org/packages/f6/c7/876a8c7a7485d5d6b5c6821201d542ef28be645aa024cfe1145b35c120c1/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:286d093f95ec98fdd92fcb955003b8a3d054b4e2cab3e2707a5039e7b50520fd", size = 1614857 }, - { url = "https://files.pythonhosted.org/packages/4f/dc/041be1dff9f23dac5f48a43323cd0789cb798342011c19a248d9c9335536/greenlet-3.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c10513330af5b8ae16f023e8ddbfb486ab355d04467c4679c5cfe4659975dd9", size = 1676034 }, -] - -[[package]] -name = "griffe" -version = "1.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0d/0c/3a471b6e31951dce2360477420d0a8d1e00dea6cf33b70f3e8c3ab6e28e1/griffe-1.15.0.tar.gz", hash = "sha256:7726e3afd6f298fbc3696e67958803e7ac843c1cfe59734b6251a40cdbfb5eea", size = 424112 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/83/3b1d03d36f224edded98e9affd0467630fc09d766c0e56fb1498cbb04a9b/griffe-1.15.0-py3-none-any.whl", hash = "sha256:6f6762661949411031f5fcda9593f586e6ce8340f0ba88921a0f2ef7a81eb9a3", size = 150705 }, -] - -[[package]] -name = "grpcio" -version = "1.76.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718 }, - { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627 }, - { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167 }, - { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267 }, - { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963 }, - { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484 }, - { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777 }, - { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014 }, - { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750 }, - { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003 }, - { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716 }, - { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522 }, - { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558 }, - { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990 }, - { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387 }, - { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668 }, - { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928 }, - { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983 }, - { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727 }, - { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799 }, - { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417 }, - { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219 }, - { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826 }, - { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550 }, - { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564 }, - { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236 }, - { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795 }, - { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214 }, - { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961 }, - { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 }, -] - -[[package]] -name = "h11" -version = "0.16.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, -] - -[[package]] -name = "h2" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "hpack" }, - { name = "hyperframe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779 }, -] - -[[package]] -name = "hpack" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 }, -] - -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, -] - -[[package]] -name = "httptools" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280 }, - { url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004 }, - { url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655 }, - { url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440 }, - { url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186 }, - { url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192 }, - { url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694 }, - { url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889 }, - { url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180 }, - { url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596 }, - { url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268 }, - { url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517 }, - { url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337 }, - { url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743 }, - { url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619 }, - { url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714 }, - { url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909 }, - { url = "https://files.pythonhosted.org/packages/e0/4a/a548bdfae6369c0d078bab5769f7b66f17f1bfaa6fa28f81d6be6959066b/httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b", size = 470831 }, - { url = "https://files.pythonhosted.org/packages/4d/31/14df99e1c43bd132eec921c2e7e11cda7852f65619bc0fc5bdc2d0cb126c/httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60", size = 452631 }, - { url = "https://files.pythonhosted.org/packages/22/d2/b7e131f7be8d854d48cb6d048113c30f9a46dca0c9a8b08fcb3fcd588cdc/httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca", size = 452910 }, - { url = "https://files.pythonhosted.org/packages/53/cf/878f3b91e4e6e011eff6d1fa9ca39f7eb17d19c9d7971b04873734112f30/httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96", size = 88205 }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, -] - -[package.optional-dependencies] -http2 = [ - { name = "h2" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960 }, -] - -[[package]] -name = "hyperframe" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 }, -] - -[[package]] -name = "idna" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "zipp" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865 }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, -] - -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, -] - -[[package]] -name = "jiter" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/45/9d/e0660989c1370e25848bb4c52d061c71837239738ad937e83edca174c273/jiter-0.12.0.tar.gz", hash = "sha256:64dfcd7d5c168b38d3f9f8bba7fc639edb3418abcc74f22fdbe6b8938293f30b", size = 168294 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/c9/5b9f7b4983f1b542c64e84165075335e8a236fa9e2ea03a0c79780062be8/jiter-0.12.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:305e061fa82f4680607a775b2e8e0bcb071cd2205ac38e6ef48c8dd5ebe1cf37", size = 314449 }, - { url = "https://files.pythonhosted.org/packages/98/6e/e8efa0e78de00db0aee82c0cf9e8b3f2027efd7f8a71f859d8f4be8e98ef/jiter-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1860627048e302a528333c9307c818c547f214d8659b0705d2195e1a94b274", size = 319855 }, - { url = "https://files.pythonhosted.org/packages/20/26/894cd88e60b5d58af53bec5c6759d1292bd0b37a8b5f60f07abf7a63ae5f/jiter-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df37577a4f8408f7e0ec3205d2a8f87672af8f17008358063a4d6425b6081ce3", size = 350171 }, - { url = "https://files.pythonhosted.org/packages/f5/27/a7b818b9979ac31b3763d25f3653ec3a954044d5e9f5d87f2f247d679fd1/jiter-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fdd787356c1c13a4f40b43c2156276ef7a71eb487d98472476476d803fb2cf", size = 365590 }, - { url = "https://files.pythonhosted.org/packages/ba/7e/e46195801a97673a83746170b17984aa8ac4a455746354516d02ca5541b4/jiter-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1eb5db8d9c65b112aacf14fcd0faae9913d07a8afea5ed06ccdd12b724e966a1", size = 479462 }, - { url = "https://files.pythonhosted.org/packages/ca/75/f833bfb009ab4bd11b1c9406d333e3b4357709ed0570bb48c7c06d78c7dd/jiter-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73c568cc27c473f82480abc15d1301adf333a7ea4f2e813d6a2c7d8b6ba8d0df", size = 378983 }, - { url = "https://files.pythonhosted.org/packages/71/b3/7a69d77943cc837d30165643db753471aff5df39692d598da880a6e51c24/jiter-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4321e8a3d868919bcb1abb1db550d41f2b5b326f72df29e53b2df8b006eb9403", size = 361328 }, - { url = "https://files.pythonhosted.org/packages/b0/ac/a78f90caf48d65ba70d8c6efc6f23150bc39dc3389d65bbec2a95c7bc628/jiter-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a51bad79f8cc9cac2b4b705039f814049142e0050f30d91695a2d9a6611f126", size = 386740 }, - { url = "https://files.pythonhosted.org/packages/39/b6/5d31c2cc8e1b6a6bcf3c5721e4ca0a3633d1ab4754b09bc7084f6c4f5327/jiter-0.12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2a67b678f6a5f1dd6c36d642d7db83e456bc8b104788262aaefc11a22339f5a9", size = 520875 }, - { url = "https://files.pythonhosted.org/packages/30/b5/4df540fae4e9f68c54b8dab004bd8c943a752f0b00efd6e7d64aa3850339/jiter-0.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efe1a211fe1fd14762adea941e3cfd6c611a136e28da6c39272dbb7a1bbe6a86", size = 511457 }, - { url = "https://files.pythonhosted.org/packages/07/65/86b74010e450a1a77b2c1aabb91d4a91dd3cd5afce99f34d75fd1ac64b19/jiter-0.12.0-cp312-cp312-win32.whl", hash = "sha256:d779d97c834b4278276ec703dc3fc1735fca50af63eb7262f05bdb4e62203d44", size = 204546 }, - { url = "https://files.pythonhosted.org/packages/1c/c7/6659f537f9562d963488e3e55573498a442503ced01f7e169e96a6110383/jiter-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e8269062060212b373316fe69236096aaf4c49022d267c6736eebd66bbbc60bb", size = 205196 }, - { url = "https://files.pythonhosted.org/packages/21/f4/935304f5169edadfec7f9c01eacbce4c90bb9a82035ac1de1f3bd2d40be6/jiter-0.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:06cb970936c65de926d648af0ed3d21857f026b1cf5525cb2947aa5e01e05789", size = 186100 }, - { url = "https://files.pythonhosted.org/packages/3d/a6/97209693b177716e22576ee1161674d1d58029eb178e01866a0422b69224/jiter-0.12.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6cc49d5130a14b732e0612bc76ae8db3b49898732223ef8b7599aa8d9810683e", size = 313658 }, - { url = "https://files.pythonhosted.org/packages/06/4d/125c5c1537c7d8ee73ad3d530a442d6c619714b95027143f1b61c0b4dfe0/jiter-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37f27a32ce36364d2fa4f7fdc507279db604d27d239ea2e044c8f148410defe1", size = 318605 }, - { url = "https://files.pythonhosted.org/packages/99/bf/a840b89847885064c41a5f52de6e312e91fa84a520848ee56c97e4fa0205/jiter-0.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbc0944aa3d4b4773e348cda635252824a78f4ba44328e042ef1ff3f6080d1cf", size = 349803 }, - { url = "https://files.pythonhosted.org/packages/8a/88/e63441c28e0db50e305ae23e19c1d8fae012d78ed55365da392c1f34b09c/jiter-0.12.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da25c62d4ee1ffbacb97fac6dfe4dcd6759ebdc9015991e92a6eae5816287f44", size = 365120 }, - { url = "https://files.pythonhosted.org/packages/0a/7c/49b02714af4343970eb8aca63396bc1c82fa01197dbb1e9b0d274b550d4e/jiter-0.12.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:048485c654b838140b007390b8182ba9774621103bd4d77c9c3f6f117474ba45", size = 479918 }, - { url = "https://files.pythonhosted.org/packages/69/ba/0a809817fdd5a1db80490b9150645f3aae16afad166960bcd562be194f3b/jiter-0.12.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:635e737fbb7315bef0037c19b88b799143d2d7d3507e61a76751025226b3ac87", size = 379008 }, - { url = "https://files.pythonhosted.org/packages/5f/c3/c9fc0232e736c8877d9e6d83d6eeb0ba4e90c6c073835cc2e8f73fdeef51/jiter-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e017c417b1ebda911bd13b1e40612704b1f5420e30695112efdbed8a4b389ed", size = 361785 }, - { url = "https://files.pythonhosted.org/packages/96/61/61f69b7e442e97ca6cd53086ddc1cf59fb830549bc72c0a293713a60c525/jiter-0.12.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:89b0bfb8b2bf2351fba36bb211ef8bfceba73ef58e7f0c68fb67b5a2795ca2f9", size = 386108 }, - { url = "https://files.pythonhosted.org/packages/e9/2e/76bb3332f28550c8f1eba3bf6e5efe211efda0ddbbaf24976bc7078d42a5/jiter-0.12.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:f5aa5427a629a824a543672778c9ce0c5e556550d1569bb6ea28a85015287626", size = 519937 }, - { url = "https://files.pythonhosted.org/packages/84/d6/fa96efa87dc8bff2094fb947f51f66368fa56d8d4fc9e77b25d7fbb23375/jiter-0.12.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed53b3d6acbcb0fd0b90f20c7cb3b24c357fe82a3518934d4edfa8c6898e498c", size = 510853 }, - { url = "https://files.pythonhosted.org/packages/8a/28/93f67fdb4d5904a708119a6ab58a8f1ec226ff10a94a282e0215402a8462/jiter-0.12.0-cp313-cp313-win32.whl", hash = "sha256:4747de73d6b8c78f2e253a2787930f4fffc68da7fa319739f57437f95963c4de", size = 204699 }, - { url = "https://files.pythonhosted.org/packages/c4/1f/30b0eb087045a0abe2a5c9c0c0c8da110875a1d3be83afd4a9a4e548be3c/jiter-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:e25012eb0c456fcc13354255d0338cd5397cce26c77b2832b3c4e2e255ea5d9a", size = 204258 }, - { url = "https://files.pythonhosted.org/packages/2c/f4/2b4daf99b96bce6fc47971890b14b2a36aef88d7beb9f057fafa032c6141/jiter-0.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:c97b92c54fe6110138c872add030a1f99aea2401ddcdaa21edf74705a646dd60", size = 185503 }, - { url = "https://files.pythonhosted.org/packages/39/ca/67bb15a7061d6fe20b9b2a2fd783e296a1e0f93468252c093481a2f00efa/jiter-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:53839b35a38f56b8be26a7851a48b89bc47e5d88e900929df10ed93b95fea3d6", size = 317965 }, - { url = "https://files.pythonhosted.org/packages/18/af/1788031cd22e29c3b14bc6ca80b16a39a0b10e611367ffd480c06a259831/jiter-0.12.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f669548e55c91ab47fef8bddd9c954dab1938644e715ea49d7e117015110a4", size = 345831 }, - { url = "https://files.pythonhosted.org/packages/05/17/710bf8472d1dff0d3caf4ced6031060091c1320f84ee7d5dcbed1f352417/jiter-0.12.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:351d54f2b09a41600ffea43d081522d792e81dcfb915f6d2d242744c1cc48beb", size = 361272 }, - { url = "https://files.pythonhosted.org/packages/fb/f1/1dcc4618b59761fef92d10bcbb0b038b5160be653b003651566a185f1a5c/jiter-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2a5e90604620f94bf62264e7c2c038704d38217b7465b863896c6d7c902b06c7", size = 204604 }, - { url = "https://files.pythonhosted.org/packages/d9/32/63cb1d9f1c5c6632a783c0052cde9ef7ba82688f7065e2f0d5f10a7e3edb/jiter-0.12.0-cp313-cp313t-win_arm64.whl", hash = "sha256:88ef757017e78d2860f96250f9393b7b577b06a956ad102c29c8237554380db3", size = 185628 }, - { url = "https://files.pythonhosted.org/packages/a8/99/45c9f0dbe4a1416b2b9a8a6d1236459540f43d7fb8883cff769a8db0612d/jiter-0.12.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c46d927acd09c67a9fb1416df45c5a04c27e83aae969267e98fba35b74e99525", size = 312478 }, - { url = "https://files.pythonhosted.org/packages/4c/a7/54ae75613ba9e0f55fcb0bc5d1f807823b5167cc944e9333ff322e9f07dd/jiter-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:774ff60b27a84a85b27b88cd5583899c59940bcc126caca97eb2a9df6aa00c49", size = 318706 }, - { url = "https://files.pythonhosted.org/packages/59/31/2aa241ad2c10774baf6c37f8b8e1f39c07db358f1329f4eb40eba179c2a2/jiter-0.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5433fab222fb072237df3f637d01b81f040a07dcac1cb4a5c75c7aa9ed0bef1", size = 351894 }, - { url = "https://files.pythonhosted.org/packages/54/4f/0f2759522719133a9042781b18cc94e335b6d290f5e2d3e6899d6af933e3/jiter-0.12.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8c593c6e71c07866ec6bfb790e202a833eeec885022296aff6b9e0b92d6a70e", size = 365714 }, - { url = "https://files.pythonhosted.org/packages/dc/6f/806b895f476582c62a2f52c453151edd8a0fde5411b0497baaa41018e878/jiter-0.12.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90d32894d4c6877a87ae00c6b915b609406819dce8bc0d4e962e4de2784e567e", size = 478989 }, - { url = "https://files.pythonhosted.org/packages/86/6c/012d894dc6e1033acd8db2b8346add33e413ec1c7c002598915278a37f79/jiter-0.12.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:798e46eed9eb10c3adbbacbd3bdb5ecd4cf7064e453d00dbef08802dae6937ff", size = 378615 }, - { url = "https://files.pythonhosted.org/packages/87/30/d718d599f6700163e28e2c71c0bbaf6dace692e7df2592fd793ac9276717/jiter-0.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3f1368f0a6719ea80013a4eb90ba72e75d7ea67cfc7846db2ca504f3df0169a", size = 364745 }, - { url = "https://files.pythonhosted.org/packages/8f/85/315b45ce4b6ddc7d7fceca24068543b02bdc8782942f4ee49d652e2cc89f/jiter-0.12.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:65f04a9d0b4406f7e51279710b27484af411896246200e461d80d3ba0caa901a", size = 386502 }, - { url = "https://files.pythonhosted.org/packages/74/0b/ce0434fb40c5b24b368fe81b17074d2840748b4952256bab451b72290a49/jiter-0.12.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:fd990541982a24281d12b67a335e44f117e4c6cbad3c3b75c7dea68bf4ce3a67", size = 519845 }, - { url = "https://files.pythonhosted.org/packages/e8/a3/7a7a4488ba052767846b9c916d208b3ed114e3eb670ee984e4c565b9cf0d/jiter-0.12.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:b111b0e9152fa7df870ecaebb0bd30240d9f7fff1f2003bcb4ed0f519941820b", size = 510701 }, - { url = "https://files.pythonhosted.org/packages/c3/16/052ffbf9d0467b70af24e30f91e0579e13ded0c17bb4a8eb2aed3cb60131/jiter-0.12.0-cp314-cp314-win32.whl", hash = "sha256:a78befb9cc0a45b5a5a0d537b06f8544c2ebb60d19d02c41ff15da28a9e22d42", size = 205029 }, - { url = "https://files.pythonhosted.org/packages/e4/18/3cf1f3f0ccc789f76b9a754bdb7a6977e5d1d671ee97a9e14f7eb728d80e/jiter-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:e1fe01c082f6aafbe5c8faf0ff074f38dfb911d53f07ec333ca03f8f6226debf", size = 204960 }, - { url = "https://files.pythonhosted.org/packages/02/68/736821e52ecfdeeb0f024b8ab01b5a229f6b9293bbdb444c27efade50b0f/jiter-0.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:d72f3b5a432a4c546ea4bedc84cce0c3404874f1d1676260b9c7f048a9855451", size = 185529 }, - { url = "https://files.pythonhosted.org/packages/30/61/12ed8ee7a643cce29ac97c2281f9ce3956eb76b037e88d290f4ed0d41480/jiter-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e6ded41aeba3603f9728ed2b6196e4df875348ab97b28fc8afff115ed42ba7a7", size = 318974 }, - { url = "https://files.pythonhosted.org/packages/2d/c6/f3041ede6d0ed5e0e79ff0de4c8f14f401bbf196f2ef3971cdbe5fd08d1d/jiter-0.12.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a947920902420a6ada6ad51892082521978e9dd44a802663b001436e4b771684", size = 345932 }, - { url = "https://files.pythonhosted.org/packages/d5/5d/4d94835889edd01ad0e2dbfc05f7bdfaed46292e7b504a6ac7839aa00edb/jiter-0.12.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:add5e227e0554d3a52cf390a7635edaffdf4f8fce4fdbcef3cc2055bb396a30c", size = 367243 }, - { url = "https://files.pythonhosted.org/packages/fd/76/0051b0ac2816253a99d27baf3dda198663aff882fa6ea7deeb94046da24e/jiter-0.12.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f9b1cda8fcb736250d7e8711d4580ebf004a46771432be0ae4796944b5dfa5d", size = 479315 }, - { url = "https://files.pythonhosted.org/packages/70/ae/83f793acd68e5cb24e483f44f482a1a15601848b9b6f199dacb970098f77/jiter-0.12.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:deeb12a2223fe0135c7ff1356a143d57f95bbf1f4a66584f1fc74df21d86b993", size = 380714 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/4808a88338ad2c228b1126b93fcd8ba145e919e886fe910d578230dabe3b/jiter-0.12.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c596cc0f4cb574877550ce4ecd51f8037469146addd676d7c1a30ebe6391923f", size = 365168 }, - { url = "https://files.pythonhosted.org/packages/0c/d4/04619a9e8095b42aef436b5aeb4c0282b4ff1b27d1db1508df9f5dc82750/jiter-0.12.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ab4c823b216a4aeab3fdbf579c5843165756bd9ad87cc6b1c65919c4715f783", size = 387893 }, - { url = "https://files.pythonhosted.org/packages/17/ea/d3c7e62e4546fdc39197fa4a4315a563a89b95b6d54c0d25373842a59cbe/jiter-0.12.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e427eee51149edf962203ff8db75a7514ab89be5cb623fb9cea1f20b54f1107b", size = 520828 }, - { url = "https://files.pythonhosted.org/packages/cc/0b/c6d3562a03fd767e31cb119d9041ea7958c3c80cb3d753eafb19b3b18349/jiter-0.12.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:edb868841f84c111255ba5e80339d386d937ec1fdce419518ce1bd9370fac5b6", size = 511009 }, - { url = "https://files.pythonhosted.org/packages/aa/51/2cb4468b3448a8385ebcd15059d325c9ce67df4e2758d133ab9442b19834/jiter-0.12.0-cp314-cp314t-win32.whl", hash = "sha256:8bbcfe2791dfdb7c5e48baf646d37a6a3dcb5a97a032017741dea9f817dca183", size = 205110 }, - { url = "https://files.pythonhosted.org/packages/b2/c5/ae5ec83dec9c2d1af805fd5fe8f74ebded9c8670c5210ec7820ce0dbeb1e/jiter-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2fa940963bf02e1d8226027ef461e36af472dea85d36054ff835aeed944dd873", size = 205223 }, - { url = "https://files.pythonhosted.org/packages/97/9a/3c5391907277f0e55195550cf3fa8e293ae9ee0c00fb402fec1e38c0c82f/jiter-0.12.0-cp314-cp314t-win_arm64.whl", hash = "sha256:506c9708dd29b27288f9f8f1140c3cb0e3d8ddb045956d7757b1fa0e0f39a473", size = 185564 }, - { url = "https://files.pythonhosted.org/packages/cb/f5/12efb8ada5f5c9edc1d4555fe383c1fb2eac05ac5859258a72d61981d999/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:e8547883d7b96ef2e5fe22b88f8a4c8725a56e7f4abafff20fd5272d634c7ecb", size = 309974 }, - { url = "https://files.pythonhosted.org/packages/85/15/d6eb3b770f6a0d332675141ab3962fd4a7c270ede3515d9f3583e1d28276/jiter-0.12.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:89163163c0934854a668ed783a2546a0617f71706a2551a4a0666d91ab365d6b", size = 304233 }, - { url = "https://files.pythonhosted.org/packages/8c/3e/e7e06743294eea2cf02ced6aa0ff2ad237367394e37a0e2b4a1108c67a36/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d96b264ab7d34bbb2312dedc47ce07cd53f06835eacbc16dde3761f47c3a9e7f", size = 338537 }, - { url = "https://files.pythonhosted.org/packages/2f/9c/6753e6522b8d0ef07d3a3d239426669e984fb0eba15a315cdbc1253904e4/jiter-0.12.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24e864cb30ab82311c6425655b0cdab0a98c5d973b065c66a3f020740c2324c", size = 346110 }, -] - -[[package]] -name = "joblib" -version = "1.5.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071 }, -] - -[[package]] -name = "jsonpath-ng" -version = "1.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ply" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6d/86/08646239a313f895186ff0a4573452038eed8c86f54380b3ebac34d32fb2/jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c", size = 37838 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/5a/73ecb3d82f8615f32ccdadeb9356726d6cae3a4bbc840b437ceb95708063/jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6", size = 30105 }, -] - -[[package]] -name = "jsonschema" -version = "4.26.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "jsonschema-specifications" }, - { name = "referencing" }, - { name = "rpds-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630 }, -] - -[[package]] -name = "jsonschema-specifications" -version = "2025.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "referencing" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437 }, -] - -[[package]] -name = "markupsafe" -version = "3.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, - { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622 }, - { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029 }, - { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374 }, - { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980 }, - { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990 }, - { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784 }, - { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588 }, - { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041 }, - { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543 }, - { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113 }, - { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911 }, - { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658 }, - { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066 }, - { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639 }, - { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569 }, - { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284 }, - { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801 }, - { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769 }, - { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642 }, - { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612 }, - { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200 }, - { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973 }, - { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619 }, - { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029 }, - { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408 }, - { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005 }, - { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048 }, - { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821 }, - { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606 }, - { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043 }, - { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747 }, - { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341 }, - { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073 }, - { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661 }, - { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069 }, - { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670 }, - { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598 }, - { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261 }, - { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835 }, - { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733 }, - { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672 }, - { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819 }, - { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426 }, - { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146 }, -] - -[[package]] -name = "mcp" -version = "1.25.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "jsonschema" }, - { name = "pydantic" }, - { name = "pydantic-settings" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "python-multipart" }, - { name = "pywin32", marker = "sys_platform == 'win32'" }, - { name = "sse-starlette" }, - { name = "starlette" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, - { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076 }, -] - -[package.optional-dependencies] -ws = [ - { name = "websockets" }, -] - -[[package]] -name = "mem0ai" -version = "1.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "openai" }, - { name = "posthog" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "pytz" }, - { name = "qdrant-client" }, - { name = "sqlalchemy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4c/b3/57edb1253e7dc24d41e102722a585d6e08a96c6191a6a04e43112c01dc5d/mem0ai-1.0.2.tar.gz", hash = "sha256:533c370e8a4e817d47a583cb7fa4df55db59de8dd67be39f2b927e2ad19607d1", size = 182395 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/82/59309070bd2d2ddccebd89d8ebb7a2155ce12531f0c36123d0a39eada544/mem0ai-1.0.2-py3-none-any.whl", hash = "sha256:3528523653bc57efa477d55e703dcedf8decc23868d4dbcc6d43a97f2315834a", size = 275428 }, -] - -[[package]] -name = "microsoft-agents-activity" -version = "0.7.0.dev12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/92/02/9c9eb63917392883ad371f1f8c534adfb68deeb0a2ffcf489951a3a5ebc6/microsoft_agents_activity-0.7.0.dev12.tar.gz", hash = "sha256:0b3d7ca7af9559729e32aa2c64aef6de4426a0d8357af7a55f5a8cded5d084a9", size = 60983 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/71/e946dfe26df5c57487c587b95e05c77f39a6a75181caad0a2e47fe2b0b70/microsoft_agents_activity-0.7.0.dev12-py3-none-any.whl", hash = "sha256:fb87ce08abe35e7e1226db34a76a2a6303989fa4f6ee3f82b39c51440d999cd8", size = 132661 }, -] - -[[package]] -name = "microsoft-agents-copilotstudio-client" -version = "0.7.0.dev12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "microsoft-agents-hosting-core" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/88/e2/ee2077a873377c3d6832fb44c339dd2b1db9b65e53e9cdd1b460daa8aef2/microsoft_agents_copilotstudio_client-0.7.0.dev12.tar.gz", hash = "sha256:cab5c1bc149bbd3b32ce3f00ecdb38ff00664f180d93f882a5e65fa738d6ff88", size = 12648 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/67/4d01168a35c4dd7ed97f9588143e3eea8f4894ce5de8401428da0dad3fc9/microsoft_agents_copilotstudio_client-0.7.0.dev12-py3-none-any.whl", hash = "sha256:c78682deb416652957992436b47c864c4287da377fe48fcd2bfef3eacf99cc75", size = 13494 }, -] - -[[package]] -name = "microsoft-agents-hosting-core" -version = "0.7.0.dev12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "isodate" }, - { name = "microsoft-agents-activity" }, - { name = "pyjwt" }, - { name = "python-dotenv" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1e/ca/ffe2f0ed6aa9f7a2a9d793539003fee0e86622b83a61f0933065de7b7953/microsoft_agents_hosting_core-0.7.0.dev12.tar.gz", hash = "sha256:8093ced5a435cb2fb177be38dd1eeaec937aefa544ec1371f65b41dd53a3721d", size = 90609 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/19/b09facedd92b439ffed675ceaf611bd9107acfcf77df531816587019f865/microsoft_agents_hosting_core-0.7.0.dev12-py3-none-any.whl", hash = "sha256:cca0d752c8ce055cc53211e0e3e501466ac629bf50f391550c9f029b791b620e", size = 133796 }, -] - -[[package]] -name = "ml-dtypes" -version = "0.5.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927 }, - { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464 }, - { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002 }, - { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222 }, - { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793 }, - { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888 }, - { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993 }, - { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956 }, - { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224 }, - { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798 }, - { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083 }, - { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111 }, - { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453 }, - { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612 }, - { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145 }, - { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781 }, - { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145 }, - { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230 }, - { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032 }, - { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353 }, - { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085 }, - { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358 }, - { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332 }, - { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612 }, - { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825 }, -] - -[[package]] -name = "msal" -version = "1.35.0b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cryptography" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/7a/6880016fab1720981b54db844c32af6f2e5e90aac21575ad6e54e1840313/msal-1.35.0b1.tar.gz", hash = "sha256:fe8143079183a5c952cd9f3ba66a148fe7bae9fb9952bd0e834272bfbeb34508", size = 157573 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/8e/7090fafcf58e9081767a8fa960431c708211ce273bc4f6e519e9046acacc/msal-1.35.0b1-py3-none-any.whl", hash = "sha256:bf656775c64bbc2103d8255980f5c3c966c7432106795e1fe70ca338a7e43150", size = 117733 }, -] - -[[package]] -name = "msal-extensions" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "msal" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, -] - -[[package]] -name = "msrest" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "azure-core" }, - { name = "certifi" }, - { name = "isodate" }, - { name = "requests" }, - { name = "requests-oauthlib" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384 }, -] - -[[package]] -name = "multidict" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/80/1e/5492c365f222f907de1039b91f922b93fa4f764c713ee858d235495d8f50/multidict-6.7.0.tar.gz", hash = "sha256:c6e99d9a65ca282e578dfea819cfa9c0a62b2499d8677392e09feaf305e9e6f5", size = 101834 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/9e/9f61ac18d9c8b475889f32ccfa91c9f59363480613fc807b6e3023d6f60b/multidict-6.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8a3862568a36d26e650a19bb5cbbba14b71789032aebc0423f8cc5f150730184", size = 76877 }, - { url = "https://files.pythonhosted.org/packages/38/6f/614f09a04e6184f8824268fce4bc925e9849edfa654ddd59f0b64508c595/multidict-6.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:960c60b5849b9b4f9dcc9bea6e3626143c252c74113df2c1540aebce70209b45", size = 45467 }, - { url = "https://files.pythonhosted.org/packages/b3/93/c4f67a436dd026f2e780c433277fff72be79152894d9fc36f44569cab1a6/multidict-6.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2049be98fb57a31b4ccf870bf377af2504d4ae35646a19037ec271e4c07998aa", size = 43834 }, - { url = "https://files.pythonhosted.org/packages/7f/f5/013798161ca665e4a422afbc5e2d9e4070142a9ff8905e482139cd09e4d0/multidict-6.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0934f3843a1860dd465d38895c17fce1f1cb37295149ab05cd1b9a03afacb2a7", size = 250545 }, - { url = "https://files.pythonhosted.org/packages/71/2f/91dbac13e0ba94669ea5119ba267c9a832f0cb65419aca75549fcf09a3dc/multidict-6.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3e34f3a1b8131ba06f1a73adab24f30934d148afcd5f5de9a73565a4404384e", size = 258305 }, - { url = "https://files.pythonhosted.org/packages/ef/b0/754038b26f6e04488b48ac621f779c341338d78503fb45403755af2df477/multidict-6.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:efbb54e98446892590dc2458c19c10344ee9a883a79b5cec4bc34d6656e8d546", size = 242363 }, - { url = "https://files.pythonhosted.org/packages/87/15/9da40b9336a7c9fa606c4cf2ed80a649dffeb42b905d4f63a1d7eb17d746/multidict-6.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a35c5fc61d4f51eb045061e7967cfe3123d622cd500e8868e7c0c592a09fedc4", size = 268375 }, - { url = "https://files.pythonhosted.org/packages/82/72/c53fcade0cc94dfaad583105fd92b3a783af2091eddcb41a6d5a52474000/multidict-6.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29fe6740ebccba4175af1b9b87bf553e9c15cd5868ee967e010efcf94e4fd0f1", size = 269346 }, - { url = "https://files.pythonhosted.org/packages/0d/e2/9baffdae21a76f77ef8447f1a05a96ec4bc0a24dae08767abc0a2fe680b8/multidict-6.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:123e2a72e20537add2f33a79e605f6191fba2afda4cbb876e35c1a7074298a7d", size = 256107 }, - { url = "https://files.pythonhosted.org/packages/3c/06/3f06f611087dc60d65ef775f1fb5aca7c6d61c6db4990e7cda0cef9b1651/multidict-6.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b284e319754366c1aee2267a2036248b24eeb17ecd5dc16022095e747f2f4304", size = 253592 }, - { url = "https://files.pythonhosted.org/packages/20/24/54e804ec7945b6023b340c412ce9c3f81e91b3bf5fa5ce65558740141bee/multidict-6.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:803d685de7be4303b5a657b76e2f6d1240e7e0a8aa2968ad5811fa2285553a12", size = 251024 }, - { url = "https://files.pythonhosted.org/packages/14/48/011cba467ea0b17ceb938315d219391d3e421dfd35928e5dbdc3f4ae76ef/multidict-6.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c04a328260dfd5db8c39538f999f02779012268f54614902d0afc775d44e0a62", size = 251484 }, - { url = "https://files.pythonhosted.org/packages/0d/2f/919258b43bb35b99fa127435cfb2d91798eb3a943396631ef43e3720dcf4/multidict-6.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8a19cdb57cd3df4cd865849d93ee14920fb97224300c88501f16ecfa2604b4e0", size = 263579 }, - { url = "https://files.pythonhosted.org/packages/31/22/a0e884d86b5242b5a74cf08e876bdf299e413016b66e55511f7a804a366e/multidict-6.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9b2fd74c52accced7e75de26023b7dccee62511a600e62311b918ec5c168fc2a", size = 259654 }, - { url = "https://files.pythonhosted.org/packages/b2/e5/17e10e1b5c5f5a40f2fcbb45953c9b215f8a4098003915e46a93f5fcaa8f/multidict-6.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3e8bfdd0e487acf992407a140d2589fe598238eaeffa3da8448d63a63cd363f8", size = 251511 }, - { url = "https://files.pythonhosted.org/packages/e3/9a/201bb1e17e7af53139597069c375e7b0dcbd47594604f65c2d5359508566/multidict-6.7.0-cp312-cp312-win32.whl", hash = "sha256:dd32a49400a2c3d52088e120ee00c1e3576cbff7e10b98467962c74fdb762ed4", size = 41895 }, - { url = "https://files.pythonhosted.org/packages/46/e2/348cd32faad84eaf1d20cce80e2bb0ef8d312c55bca1f7fa9865e7770aaf/multidict-6.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:92abb658ef2d7ef22ac9f8bb88e8b6c3e571671534e029359b6d9e845923eb1b", size = 46073 }, - { url = "https://files.pythonhosted.org/packages/25/ec/aad2613c1910dce907480e0c3aa306905830f25df2e54ccc9dea450cb5aa/multidict-6.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:490dab541a6a642ce1a9d61a4781656b346a55c13038f0b1244653828e3a83ec", size = 43226 }, - { url = "https://files.pythonhosted.org/packages/d2/86/33272a544eeb36d66e4d9a920602d1a2f57d4ebea4ef3cdfe5a912574c95/multidict-6.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bee7c0588aa0076ce77c0ea5d19a68d76ad81fcd9fe8501003b9a24f9d4000f6", size = 76135 }, - { url = "https://files.pythonhosted.org/packages/91/1c/eb97db117a1ebe46d457a3d235a7b9d2e6dcab174f42d1b67663dd9e5371/multidict-6.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7ef6b61cad77091056ce0e7ce69814ef72afacb150b7ac6a3e9470def2198159", size = 45117 }, - { url = "https://files.pythonhosted.org/packages/f1/d8/6c3442322e41fb1dd4de8bd67bfd11cd72352ac131f6368315617de752f1/multidict-6.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c0359b1ec12b1d6849c59f9d319610b7f20ef990a6d454ab151aa0e3b9f78ca", size = 43472 }, - { url = "https://files.pythonhosted.org/packages/75/3f/e2639e80325af0b6c6febdf8e57cc07043ff15f57fa1ef808f4ccb5ac4cd/multidict-6.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cd240939f71c64bd658f186330603aac1a9a81bf6273f523fca63673cb7378a8", size = 249342 }, - { url = "https://files.pythonhosted.org/packages/5d/cc/84e0585f805cbeaa9cbdaa95f9a3d6aed745b9d25700623ac89a6ecff400/multidict-6.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60a4d75718a5efa473ebd5ab685786ba0c67b8381f781d1be14da49f1a2dc60", size = 257082 }, - { url = "https://files.pythonhosted.org/packages/b0/9c/ac851c107c92289acbbf5cfb485694084690c1b17e555f44952c26ddc5bd/multidict-6.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53a42d364f323275126aff81fb67c5ca1b7a04fda0546245730a55c8c5f24bc4", size = 240704 }, - { url = "https://files.pythonhosted.org/packages/50/cc/5f93e99427248c09da95b62d64b25748a5f5c98c7c2ab09825a1d6af0e15/multidict-6.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3b29b980d0ddbecb736735ee5bef69bb2ddca56eff603c86f3f29a1128299b4f", size = 266355 }, - { url = "https://files.pythonhosted.org/packages/ec/0c/2ec1d883ceb79c6f7f6d7ad90c919c898f5d1c6ea96d322751420211e072/multidict-6.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8a93b1c0ed2d04b97a5e9336fd2d33371b9a6e29ab7dd6503d63407c20ffbaf", size = 267259 }, - { url = "https://files.pythonhosted.org/packages/c6/2d/f0b184fa88d6630aa267680bdb8623fb69cb0d024b8c6f0d23f9a0f406d3/multidict-6.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff96e8815eecacc6645da76c413eb3b3d34cfca256c70b16b286a687d013c32", size = 254903 }, - { url = "https://files.pythonhosted.org/packages/06/c9/11ea263ad0df7dfabcad404feb3c0dd40b131bc7f232d5537f2fb1356951/multidict-6.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7516c579652f6a6be0e266aec0acd0db80829ca305c3d771ed898538804c2036", size = 252365 }, - { url = "https://files.pythonhosted.org/packages/41/88/d714b86ee2c17d6e09850c70c9d310abac3d808ab49dfa16b43aba9d53fd/multidict-6.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:040f393368e63fb0f3330e70c26bfd336656bed925e5cbe17c9da839a6ab13ec", size = 250062 }, - { url = "https://files.pythonhosted.org/packages/15/fe/ad407bb9e818c2b31383f6131ca19ea7e35ce93cf1310fce69f12e89de75/multidict-6.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b3bc26a951007b1057a1c543af845f1c7e3e71cc240ed1ace7bf4484aa99196e", size = 249683 }, - { url = "https://files.pythonhosted.org/packages/8c/a4/a89abdb0229e533fb925e7c6e5c40201c2873efebc9abaf14046a4536ee6/multidict-6.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7b022717c748dd1992a83e219587aabe45980d88969f01b316e78683e6285f64", size = 261254 }, - { url = "https://files.pythonhosted.org/packages/8d/aa/0e2b27bd88b40a4fb8dc53dd74eecac70edaa4c1dd0707eb2164da3675b3/multidict-6.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9600082733859f00d79dee64effc7aef1beb26adb297416a4ad2116fd61374bd", size = 257967 }, - { url = "https://files.pythonhosted.org/packages/d0/8e/0c67b7120d5d5f6d874ed85a085f9dc770a7f9d8813e80f44a9fec820bb7/multidict-6.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94218fcec4d72bc61df51c198d098ce2b378e0ccbac41ddbed5ef44092913288", size = 250085 }, - { url = "https://files.pythonhosted.org/packages/ba/55/b73e1d624ea4b8fd4dd07a3bb70f6e4c7c6c5d9d640a41c6ffe5cdbd2a55/multidict-6.7.0-cp313-cp313-win32.whl", hash = "sha256:a37bd74c3fa9d00be2d7b8eca074dc56bd8077ddd2917a839bd989612671ed17", size = 41713 }, - { url = "https://files.pythonhosted.org/packages/32/31/75c59e7d3b4205075b4c183fa4ca398a2daf2303ddf616b04ae6ef55cffe/multidict-6.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:30d193c6cc6d559db42b6bcec8a5d395d34d60c9877a0b71ecd7c204fcf15390", size = 45915 }, - { url = "https://files.pythonhosted.org/packages/31/2a/8987831e811f1184c22bc2e45844934385363ee61c0a2dcfa8f71b87e608/multidict-6.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:ea3334cabe4d41b7ccd01e4d349828678794edbc2d3ae97fc162a3312095092e", size = 43077 }, - { url = "https://files.pythonhosted.org/packages/e8/68/7b3a5170a382a340147337b300b9eb25a9ddb573bcdfff19c0fa3f31ffba/multidict-6.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ad9ce259f50abd98a1ca0aa6e490b58c316a0fce0617f609723e40804add2c00", size = 83114 }, - { url = "https://files.pythonhosted.org/packages/55/5c/3fa2d07c84df4e302060f555bbf539310980362236ad49f50eeb0a1c1eb9/multidict-6.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07f5594ac6d084cbb5de2df218d78baf55ef150b91f0ff8a21cc7a2e3a5a58eb", size = 48442 }, - { url = "https://files.pythonhosted.org/packages/fc/56/67212d33239797f9bd91962bb899d72bb0f4c35a8652dcdb8ed049bef878/multidict-6.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0591b48acf279821a579282444814a2d8d0af624ae0bc600aa4d1b920b6e924b", size = 46885 }, - { url = "https://files.pythonhosted.org/packages/46/d1/908f896224290350721597a61a69cd19b89ad8ee0ae1f38b3f5cd12ea2ac/multidict-6.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:749a72584761531d2b9467cfbdfd29487ee21124c304c4b6cb760d8777b27f9c", size = 242588 }, - { url = "https://files.pythonhosted.org/packages/ab/67/8604288bbd68680eee0ab568fdcb56171d8b23a01bcd5cb0c8fedf6e5d99/multidict-6.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b4c3d199f953acd5b446bf7c0de1fe25d94e09e79086f8dc2f48a11a129cdf1", size = 249966 }, - { url = "https://files.pythonhosted.org/packages/20/33/9228d76339f1ba51e3efef7da3ebd91964d3006217aae13211653193c3ff/multidict-6.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9fb0211dfc3b51efea2f349ec92c114d7754dd62c01f81c3e32b765b70c45c9b", size = 228618 }, - { url = "https://files.pythonhosted.org/packages/f8/2d/25d9b566d10cab1c42b3b9e5b11ef79c9111eaf4463b8c257a3bd89e0ead/multidict-6.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a027ec240fe73a8d6281872690b988eed307cd7d91b23998ff35ff577ca688b5", size = 257539 }, - { url = "https://files.pythonhosted.org/packages/b6/b1/8d1a965e6637fc33de3c0d8f414485c2b7e4af00f42cab3d84e7b955c222/multidict-6.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d964afecdf3a8288789df2f5751dc0a8261138c3768d9af117ed384e538fad", size = 256345 }, - { url = "https://files.pythonhosted.org/packages/ba/0c/06b5a8adbdeedada6f4fb8d8f193d44a347223b11939b42953eeb6530b6b/multidict-6.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caf53b15b1b7df9fbd0709aa01409000a2b4dd03a5f6f5cc548183c7c8f8b63c", size = 247934 }, - { url = "https://files.pythonhosted.org/packages/8f/31/b2491b5fe167ca044c6eb4b8f2c9f3b8a00b24c432c365358eadac5d7625/multidict-6.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:654030da3197d927f05a536a66186070e98765aa5142794c9904555d3a9d8fb5", size = 245243 }, - { url = "https://files.pythonhosted.org/packages/61/1a/982913957cb90406c8c94f53001abd9eafc271cb3e70ff6371590bec478e/multidict-6.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2090d3718829d1e484706a2f525e50c892237b2bf9b17a79b059cb98cddc2f10", size = 235878 }, - { url = "https://files.pythonhosted.org/packages/be/c0/21435d804c1a1cf7a2608593f4d19bca5bcbd7a81a70b253fdd1c12af9c0/multidict-6.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d2cfeec3f6f45651b3d408c4acec0ebf3daa9bc8a112a084206f5db5d05b754", size = 243452 }, - { url = "https://files.pythonhosted.org/packages/54/0a/4349d540d4a883863191be6eb9a928846d4ec0ea007d3dcd36323bb058ac/multidict-6.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:4ef089f985b8c194d341eb2c24ae6e7408c9a0e2e5658699c92f497437d88c3c", size = 252312 }, - { url = "https://files.pythonhosted.org/packages/26/64/d5416038dbda1488daf16b676e4dbfd9674dde10a0cc8f4fc2b502d8125d/multidict-6.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e93a0617cd16998784bf4414c7e40f17a35d2350e5c6f0bd900d3a8e02bd3762", size = 246935 }, - { url = "https://files.pythonhosted.org/packages/9f/8c/8290c50d14e49f35e0bd4abc25e1bc7711149ca9588ab7d04f886cdf03d9/multidict-6.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0feece2ef8ebc42ed9e2e8c78fc4aa3cf455733b507c09ef7406364c94376c6", size = 243385 }, - { url = "https://files.pythonhosted.org/packages/ef/a0/f83ae75e42d694b3fbad3e047670e511c138be747bc713cf1b10d5096416/multidict-6.7.0-cp313-cp313t-win32.whl", hash = "sha256:19a1d55338ec1be74ef62440ca9e04a2f001a04d0cc49a4983dc320ff0f3212d", size = 47777 }, - { url = "https://files.pythonhosted.org/packages/dc/80/9b174a92814a3830b7357307a792300f42c9e94664b01dee8e457551fa66/multidict-6.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3da4fb467498df97e986af166b12d01f05d2e04f978a9c1c680ea1988e0bc4b6", size = 53104 }, - { url = "https://files.pythonhosted.org/packages/cc/28/04baeaf0428d95bb7a7bea0e691ba2f31394338ba424fb0679a9ed0f4c09/multidict-6.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:b4121773c49a0776461f4a904cdf6264c88e42218aaa8407e803ca8025872792", size = 45503 }, - { url = "https://files.pythonhosted.org/packages/e2/b1/3da6934455dd4b261d4c72f897e3a5728eba81db59959f3a639245891baa/multidict-6.7.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3bab1e4aff7adaa34410f93b1f8e57c4b36b9af0426a76003f441ee1d3c7e842", size = 75128 }, - { url = "https://files.pythonhosted.org/packages/14/2c/f069cab5b51d175a1a2cb4ccdf7a2c2dabd58aa5bd933fa036a8d15e2404/multidict-6.7.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b8512bac933afc3e45fb2b18da8e59b78d4f408399a960339598374d4ae3b56b", size = 44410 }, - { url = "https://files.pythonhosted.org/packages/42/e2/64bb41266427af6642b6b128e8774ed84c11b80a90702c13ac0a86bb10cc/multidict-6.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:79dcf9e477bc65414ebfea98ffd013cb39552b5ecd62908752e0e413d6d06e38", size = 43205 }, - { url = "https://files.pythonhosted.org/packages/02/68/6b086fef8a3f1a8541b9236c594f0c9245617c29841f2e0395d979485cde/multidict-6.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:31bae522710064b5cbeddaf2e9f32b1abab70ac6ac91d42572502299e9953128", size = 245084 }, - { url = "https://files.pythonhosted.org/packages/15/ee/f524093232007cd7a75c1d132df70f235cfd590a7c9eaccd7ff422ef4ae8/multidict-6.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a0df7ff02397bb63e2fd22af2c87dfa39e8c7f12947bc524dbdc528282c7e34", size = 252667 }, - { url = "https://files.pythonhosted.org/packages/02/a5/eeb3f43ab45878f1895118c3ef157a480db58ede3f248e29b5354139c2c9/multidict-6.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a0222514e8e4c514660e182d5156a415c13ef0aabbd71682fc714e327b95e99", size = 233590 }, - { url = "https://files.pythonhosted.org/packages/6a/1e/76d02f8270b97269d7e3dbd45644b1785bda457b474315f8cf999525a193/multidict-6.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2397ab4daaf2698eb51a76721e98db21ce4f52339e535725de03ea962b5a3202", size = 264112 }, - { url = "https://files.pythonhosted.org/packages/76/0b/c28a70ecb58963847c2a8efe334904cd254812b10e535aefb3bcce513918/multidict-6.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8891681594162635948a636c9fe0ff21746aeb3dd5463f6e25d9bea3a8a39ca1", size = 261194 }, - { url = "https://files.pythonhosted.org/packages/b4/63/2ab26e4209773223159b83aa32721b4021ffb08102f8ac7d689c943fded1/multidict-6.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18706cc31dbf402a7945916dd5cddf160251b6dab8a2c5f3d6d5a55949f676b3", size = 248510 }, - { url = "https://files.pythonhosted.org/packages/93/cd/06c1fa8282af1d1c46fd55c10a7930af652afdce43999501d4d68664170c/multidict-6.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f844a1bbf1d207dd311a56f383f7eda2d0e134921d45751842d8235e7778965d", size = 248395 }, - { url = "https://files.pythonhosted.org/packages/99/ac/82cb419dd6b04ccf9e7e61befc00c77614fc8134362488b553402ecd55ce/multidict-6.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d4393e3581e84e5645506923816b9cc81f5609a778c7e7534054091acc64d1c6", size = 239520 }, - { url = "https://files.pythonhosted.org/packages/fa/f3/a0f9bf09493421bd8716a362e0cd1d244f5a6550f5beffdd6b47e885b331/multidict-6.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:fbd18dc82d7bf274b37aa48d664534330af744e03bccf696d6f4c6042e7d19e7", size = 245479 }, - { url = "https://files.pythonhosted.org/packages/8d/01/476d38fc73a212843f43c852b0eee266b6971f0e28329c2184a8df90c376/multidict-6.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b6234e14f9314731ec45c42fc4554b88133ad53a09092cc48a88e771c125dadb", size = 258903 }, - { url = "https://files.pythonhosted.org/packages/49/6d/23faeb0868adba613b817d0e69c5f15531b24d462af8012c4f6de4fa8dc3/multidict-6.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:08d4379f9744d8f78d98c8673c06e202ffa88296f009c71bbafe8a6bf847d01f", size = 252333 }, - { url = "https://files.pythonhosted.org/packages/1e/cc/48d02ac22b30fa247f7dad82866e4b1015431092f4ba6ebc7e77596e0b18/multidict-6.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9fe04da3f79387f450fd0061d4dd2e45a72749d31bf634aecc9e27f24fdc4b3f", size = 243411 }, - { url = "https://files.pythonhosted.org/packages/4a/03/29a8bf5a18abf1fe34535c88adbdfa88c9fb869b5a3b120692c64abe8284/multidict-6.7.0-cp314-cp314-win32.whl", hash = "sha256:fbafe31d191dfa7c4c51f7a6149c9fb7e914dcf9ffead27dcfd9f1ae382b3885", size = 40940 }, - { url = "https://files.pythonhosted.org/packages/82/16/7ed27b680791b939de138f906d5cf2b4657b0d45ca6f5dd6236fdddafb1a/multidict-6.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2f67396ec0310764b9222a1728ced1ab638f61aadc6226f17a71dd9324f9a99c", size = 45087 }, - { url = "https://files.pythonhosted.org/packages/cd/3c/e3e62eb35a1950292fe39315d3c89941e30a9d07d5d2df42965ab041da43/multidict-6.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:ba672b26069957ee369cfa7fc180dde1fc6f176eaf1e6beaf61fbebbd3d9c000", size = 42368 }, - { url = "https://files.pythonhosted.org/packages/8b/40/cd499bd0dbc5f1136726db3153042a735fffd0d77268e2ee20d5f33c010f/multidict-6.7.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:c1dcc7524066fa918c6a27d61444d4ee7900ec635779058571f70d042d86ed63", size = 82326 }, - { url = "https://files.pythonhosted.org/packages/13/8a/18e031eca251c8df76daf0288e6790561806e439f5ce99a170b4af30676b/multidict-6.7.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:27e0b36c2d388dc7b6ced3406671b401e84ad7eb0656b8f3a2f46ed0ce483718", size = 48065 }, - { url = "https://files.pythonhosted.org/packages/40/71/5e6701277470a87d234e433fb0a3a7deaf3bcd92566e421e7ae9776319de/multidict-6.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a7baa46a22e77f0988e3b23d4ede5513ebec1929e34ee9495be535662c0dfe2", size = 46475 }, - { url = "https://files.pythonhosted.org/packages/fe/6a/bab00cbab6d9cfb57afe1663318f72ec28289ea03fd4e8236bb78429893a/multidict-6.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bf77f54997a9166a2f5675d1201520586439424c2511723a7312bdb4bcc034e", size = 239324 }, - { url = "https://files.pythonhosted.org/packages/2a/5f/8de95f629fc22a7769ade8b41028e3e5a822c1f8904f618d175945a81ad3/multidict-6.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e011555abada53f1578d63389610ac8a5400fc70ce71156b0aa30d326f1a5064", size = 246877 }, - { url = "https://files.pythonhosted.org/packages/23/b4/38881a960458f25b89e9f4a4fdcb02ac101cfa710190db6e5528841e67de/multidict-6.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:28b37063541b897fd6a318007373930a75ca6d6ac7c940dbe14731ffdd8d498e", size = 225824 }, - { url = "https://files.pythonhosted.org/packages/1e/39/6566210c83f8a261575f18e7144736059f0c460b362e96e9cf797a24b8e7/multidict-6.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05047ada7a2fde2631a0ed706f1fd68b169a681dfe5e4cf0f8e4cb6618bbc2cd", size = 253558 }, - { url = "https://files.pythonhosted.org/packages/00/a3/67f18315100f64c269f46e6c0319fa87ba68f0f64f2b8e7fd7c72b913a0b/multidict-6.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:716133f7d1d946a4e1b91b1756b23c088881e70ff180c24e864c26192ad7534a", size = 252339 }, - { url = "https://files.pythonhosted.org/packages/c8/2a/1cb77266afee2458d82f50da41beba02159b1d6b1f7973afc9a1cad1499b/multidict-6.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d1bed1b467ef657f2a0ae62844a607909ef1c6889562de5e1d505f74457d0b96", size = 244895 }, - { url = "https://files.pythonhosted.org/packages/dd/72/09fa7dd487f119b2eb9524946ddd36e2067c08510576d43ff68469563b3b/multidict-6.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca43bdfa5d37bd6aee89d85e1d0831fb86e25541be7e9d376ead1b28974f8e5e", size = 241862 }, - { url = "https://files.pythonhosted.org/packages/65/92/bc1f8bd0853d8669300f732c801974dfc3702c3eeadae2f60cef54dc69d7/multidict-6.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:44b546bd3eb645fd26fb949e43c02a25a2e632e2ca21a35e2e132c8105dc8599", size = 232376 }, - { url = "https://files.pythonhosted.org/packages/09/86/ac39399e5cb9d0c2ac8ef6e10a768e4d3bc933ac808d49c41f9dc23337eb/multidict-6.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a6ef16328011d3f468e7ebc326f24c1445f001ca1dec335b2f8e66bed3006394", size = 240272 }, - { url = "https://files.pythonhosted.org/packages/3d/b6/fed5ac6b8563ec72df6cb1ea8dac6d17f0a4a1f65045f66b6d3bf1497c02/multidict-6.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5aa873cbc8e593d361ae65c68f85faadd755c3295ea2c12040ee146802f23b38", size = 248774 }, - { url = "https://files.pythonhosted.org/packages/6b/8d/b954d8c0dc132b68f760aefd45870978deec6818897389dace00fcde32ff/multidict-6.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d7b6ccce016e29df4b7ca819659f516f0bc7a4b3efa3bb2012ba06431b044f9", size = 242731 }, - { url = "https://files.pythonhosted.org/packages/16/9d/a2dac7009125d3540c2f54e194829ea18ac53716c61b655d8ed300120b0f/multidict-6.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:171b73bd4ee683d307599b66793ac80981b06f069b62eea1c9e29c9241aa66b0", size = 240193 }, - { url = "https://files.pythonhosted.org/packages/39/ca/c05f144128ea232ae2178b008d5011d4e2cea86e4ee8c85c2631b1b94802/multidict-6.7.0-cp314-cp314t-win32.whl", hash = "sha256:b2d7f80c4e1fd010b07cb26820aae86b7e73b681ee4889684fb8d2d4537aab13", size = 48023 }, - { url = "https://files.pythonhosted.org/packages/ba/8f/0a60e501584145588be1af5cc829265701ba3c35a64aec8e07cbb71d39bb/multidict-6.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:09929cab6fcb68122776d575e03c6cc64ee0b8fca48d17e135474b042ce515cd", size = 53507 }, - { url = "https://files.pythonhosted.org/packages/7f/ae/3148b988a9c6239903e786eac19c889fab607c31d6efa7fb2147e5680f23/multidict-6.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:cc41db090ed742f32bd2d2c721861725e6109681eddf835d0a82bd3a5c382827", size = 44804 }, - { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317 }, -] - -[[package]] -name = "nltk" -version = "3.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "joblib" }, - { name = "regex" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f9/76/3a5e4312c19a028770f86fd7c058cf9f4ec4321c6cf7526bab998a5b683c/nltk-3.9.2.tar.gz", hash = "sha256:0f409e9b069ca4177c1903c3e843eef90c7e92992fa4931ae607da6de49e1419", size = 2887629 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/90/81ac364ef94209c100e12579629dc92bf7a709a84af32f8c551b02c07e94/nltk-3.9.2-py3-none-any.whl", hash = "sha256:1e209d2b3009110635ed9709a67a1a3e33a10f799490fa71cf4bec218c11c88a", size = 1513404 }, -] - -[[package]] -name = "numpy" -version = "2.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/62/ae72ff66c0f1fd959925b4c11f8c2dea61f47f6acaea75a08512cdfe3fed/numpy-2.4.1.tar.gz", hash = "sha256:a1ceafc5042451a858231588a104093474c6a5c57dcc724841f5c888d237d690", size = 20721320 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/7f/ec53e32bf10c813604edf07a3682616bd931d026fcde7b6d13195dfb684a/numpy-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d3703409aac693fa82c0aee023a1ae06a6e9d065dba10f5e8e80f642f1e9d0a2", size = 16656888 }, - { url = "https://files.pythonhosted.org/packages/b8/e0/1f9585d7dae8f14864e948fd7fa86c6cb72dee2676ca2748e63b1c5acfe0/numpy-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7211b95ca365519d3596a1d8688a95874cc94219d417504d9ecb2df99fa7bfa8", size = 12373956 }, - { url = "https://files.pythonhosted.org/packages/8e/43/9762e88909ff2326f5e7536fa8cb3c49fb03a7d92705f23e6e7f553d9cb3/numpy-2.4.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5adf01965456a664fc727ed69cc71848f28d063217c63e1a0e200a118d5eec9a", size = 5202567 }, - { url = "https://files.pythonhosted.org/packages/4b/ee/34b7930eb61e79feb4478800a4b95b46566969d837546aa7c034c742ef98/numpy-2.4.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:26f0bcd9c79a00e339565b303badc74d3ea2bd6d52191eeca5f95936cad107d0", size = 6549459 }, - { url = "https://files.pythonhosted.org/packages/79/e3/5f115fae982565771be994867c89bcd8d7208dbfe9469185497d70de5ddf/numpy-2.4.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0093e85df2960d7e4049664b26afc58b03236e967fb942354deef3208857a04c", size = 14404859 }, - { url = "https://files.pythonhosted.org/packages/d9/7d/9c8a781c88933725445a859cac5d01b5871588a15969ee6aeb618ba99eee/numpy-2.4.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad270f438cbdd402c364980317fb6b117d9ec5e226fff5b4148dd9aa9fc6e02", size = 16371419 }, - { url = "https://files.pythonhosted.org/packages/a6/d2/8aa084818554543f17cf4162c42f162acbd3bb42688aefdba6628a859f77/numpy-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:297c72b1b98100c2e8f873d5d35fb551fce7040ade83d67dd51d38c8d42a2162", size = 16182131 }, - { url = "https://files.pythonhosted.org/packages/60/db/0425216684297c58a8df35f3284ef56ec4a043e6d283f8a59c53562caf1b/numpy-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf6470d91d34bf669f61d515499859fa7a4c2f7c36434afb70e82df7217933f9", size = 18295342 }, - { url = "https://files.pythonhosted.org/packages/31/4c/14cb9d86240bd8c386c881bafbe43f001284b7cce3bc01623ac9475da163/numpy-2.4.1-cp312-cp312-win32.whl", hash = "sha256:b6bcf39112e956594b3331316d90c90c90fb961e39696bda97b89462f5f3943f", size = 5959015 }, - { url = "https://files.pythonhosted.org/packages/51/cf/52a703dbeb0c65807540d29699fef5fda073434ff61846a564d5c296420f/numpy-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:e1a27bb1b2dee45a2a53f5ca6ff2d1a7f135287883a1689e930d44d1ff296c87", size = 12310730 }, - { url = "https://files.pythonhosted.org/packages/69/80/a828b2d0ade5e74a9fe0f4e0a17c30fdc26232ad2bc8c9f8b3197cf7cf18/numpy-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:0e6e8f9d9ecf95399982019c01223dc130542960a12edfa8edd1122dfa66a8a8", size = 10312166 }, - { url = "https://files.pythonhosted.org/packages/04/68/732d4b7811c00775f3bd522a21e8dd5a23f77eb11acdeb663e4a4ebf0ef4/numpy-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d797454e37570cfd61143b73b8debd623c3c0952959adb817dd310a483d58a1b", size = 16652495 }, - { url = "https://files.pythonhosted.org/packages/20/ca/857722353421a27f1465652b2c66813eeeccea9d76d5f7b74b99f298e60e/numpy-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c55962006156aeef1629b953fd359064aa47e4d82cfc8e67f0918f7da3344f", size = 12368657 }, - { url = "https://files.pythonhosted.org/packages/81/0d/2377c917513449cc6240031a79d30eb9a163d32a91e79e0da47c43f2c0c8/numpy-2.4.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:71abbea030f2cfc3092a0ff9f8c8fdefdc5e0bf7d9d9c99663538bb0ecdac0b9", size = 5197256 }, - { url = "https://files.pythonhosted.org/packages/17/39/569452228de3f5de9064ac75137082c6214be1f5c532016549a7923ab4b5/numpy-2.4.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b55aa56165b17aaf15520beb9cbd33c9039810e0d9643dd4379e44294c7303e", size = 6545212 }, - { url = "https://files.pythonhosted.org/packages/8c/a4/77333f4d1e4dac4395385482557aeecf4826e6ff517e32ca48e1dafbe42a/numpy-2.4.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0faba4a331195bfa96f93dd9dfaa10b2c7aa8cda3a02b7fd635e588fe821bf5", size = 14402871 }, - { url = "https://files.pythonhosted.org/packages/ba/87/d341e519956273b39d8d47969dd1eaa1af740615394fe67d06f1efa68773/numpy-2.4.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e3087f53e2b4428766b54932644d148613c5a595150533ae7f00dab2f319a8", size = 16359305 }, - { url = "https://files.pythonhosted.org/packages/32/91/789132c6666288eaa20ae8066bb99eba1939362e8f1a534949a215246e97/numpy-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:49e792ec351315e16da54b543db06ca8a86985ab682602d90c60ef4ff4db2a9c", size = 16181909 }, - { url = "https://files.pythonhosted.org/packages/cf/b8/090b8bd27b82a844bb22ff8fdf7935cb1980b48d6e439ae116f53cdc2143/numpy-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79e9e06c4c2379db47f3f6fc7a8652e7498251789bf8ff5bd43bf478ef314ca2", size = 18284380 }, - { url = "https://files.pythonhosted.org/packages/67/78/722b62bd31842ff029412271556a1a27a98f45359dea78b1548a3a9996aa/numpy-2.4.1-cp313-cp313-win32.whl", hash = "sha256:3d1a100e48cb266090a031397863ff8a30050ceefd798f686ff92c67a486753d", size = 5957089 }, - { url = "https://files.pythonhosted.org/packages/da/a6/cf32198b0b6e18d4fbfa9a21a992a7fca535b9bb2b0cdd217d4a3445b5ca/numpy-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:92a0e65272fd60bfa0d9278e0484c2f52fe03b97aedc02b357f33fe752c52ffb", size = 12307230 }, - { url = "https://files.pythonhosted.org/packages/44/6c/534d692bfb7d0afe30611320c5fb713659dcb5104d7cc182aff2aea092f5/numpy-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:20d4649c773f66cc2fc36f663e091f57c3b7655f936a4c681b4250855d1da8f5", size = 10313125 }, - { url = "https://files.pythonhosted.org/packages/da/a1/354583ac5c4caa566de6ddfbc42744409b515039e085fab6e0ff942e0df5/numpy-2.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f93bc6892fe7b0663e5ffa83b61aab510aacffd58c16e012bb9352d489d90cb7", size = 12496156 }, - { url = "https://files.pythonhosted.org/packages/51/b0/42807c6e8cce58c00127b1dc24d365305189991f2a7917aa694a109c8d7d/numpy-2.4.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:178de8f87948163d98a4c9ab5bee4ce6519ca918926ec8df195af582de28544d", size = 5324663 }, - { url = "https://files.pythonhosted.org/packages/fe/55/7a621694010d92375ed82f312b2f28017694ed784775269115323e37f5e2/numpy-2.4.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:98b35775e03ab7f868908b524fc0a84d38932d8daf7b7e1c3c3a1b6c7a2c9f15", size = 6645224 }, - { url = "https://files.pythonhosted.org/packages/50/96/9fa8635ed9d7c847d87e30c834f7109fac5e88549d79ef3324ab5c20919f/numpy-2.4.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:941c2a93313d030f219f3a71fd3d91a728b82979a5e8034eb2e60d394a2b83f9", size = 14462352 }, - { url = "https://files.pythonhosted.org/packages/03/d1/8cf62d8bb2062da4fb82dd5d49e47c923f9c0738032f054e0a75342faba7/numpy-2.4.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:529050522e983e00a6c1c6b67411083630de8b57f65e853d7b03d9281b8694d2", size = 16407279 }, - { url = "https://files.pythonhosted.org/packages/86/1c/95c86e17c6b0b31ce6ef219da00f71113b220bcb14938c8d9a05cee0ff53/numpy-2.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2302dc0224c1cbc49bb94f7064f3f923a971bfae45c33870dcbff63a2a550505", size = 16248316 }, - { url = "https://files.pythonhosted.org/packages/30/b4/e7f5ff8697274c9d0fa82398b6a372a27e5cef069b37df6355ccb1f1db1a/numpy-2.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9171a42fcad32dcf3fa86f0a4faa5e9f8facefdb276f54b8b390d90447cff4e2", size = 18329884 }, - { url = "https://files.pythonhosted.org/packages/37/a4/b073f3e9d77f9aec8debe8ca7f9f6a09e888ad1ba7488f0c3b36a94c03ac/numpy-2.4.1-cp313-cp313t-win32.whl", hash = "sha256:382ad67d99ef49024f11d1ce5dcb5ad8432446e4246a4b014418ba3a1175a1f4", size = 6081138 }, - { url = "https://files.pythonhosted.org/packages/16/16/af42337b53844e67752a092481ab869c0523bc95c4e5c98e4dac4e9581ac/numpy-2.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:62fea415f83ad8fdb6c20840578e5fbaf5ddd65e0ec6c3c47eda0f69da172510", size = 12447478 }, - { url = "https://files.pythonhosted.org/packages/6c/f8/fa85b2eac68ec631d0b631abc448552cb17d39afd17ec53dcbcc3537681a/numpy-2.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a7870e8c5fc11aef57d6fea4b4085e537a3a60ad2cdd14322ed531fdca68d261", size = 10382981 }, - { url = "https://files.pythonhosted.org/packages/1b/a7/ef08d25698e0e4b4efbad8d55251d20fe2a15f6d9aa7c9b30cd03c165e6f/numpy-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3869ea1ee1a1edc16c29bbe3a2f2a4e515cc3a44d43903ad41e0cacdbaf733dc", size = 16652046 }, - { url = "https://files.pythonhosted.org/packages/8f/39/e378b3e3ca13477e5ac70293ec027c438d1927f18637e396fe90b1addd72/numpy-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e867df947d427cdd7a60e3e271729090b0f0df80f5f10ab7dd436f40811699c3", size = 12378858 }, - { url = "https://files.pythonhosted.org/packages/c3/74/7ec6154f0006910ed1fdbb7591cf4432307033102b8a22041599935f8969/numpy-2.4.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:e3bd2cb07841166420d2fa7146c96ce00cb3410664cbc1a6be028e456c4ee220", size = 5207417 }, - { url = "https://files.pythonhosted.org/packages/f7/b7/053ac11820d84e42f8feea5cb81cc4fcd1091499b45b1ed8c7415b1bf831/numpy-2.4.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:f0a90aba7d521e6954670550e561a4cb925713bd944445dbe9e729b71f6cabee", size = 6542643 }, - { url = "https://files.pythonhosted.org/packages/c0/c4/2e7908915c0e32ca636b92e4e4a3bdec4cb1e7eb0f8aedf1ed3c68a0d8cd/numpy-2.4.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d558123217a83b2d1ba316b986e9248a1ed1971ad495963d555ccd75dcb1556", size = 14418963 }, - { url = "https://files.pythonhosted.org/packages/eb/c0/3ed5083d94e7ffd7c404e54619c088e11f2e1939a9544f5397f4adb1b8ba/numpy-2.4.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f44de05659b67d20499cbc96d49f2650769afcb398b79b324bb6e297bfe3844", size = 16363811 }, - { url = "https://files.pythonhosted.org/packages/0e/68/42b66f1852bf525050a67315a4fb94586ab7e9eaa541b1bef530fab0c5dd/numpy-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:69e7419c9012c4aaf695109564e3387f1259f001b4326dfa55907b098af082d3", size = 16197643 }, - { url = "https://files.pythonhosted.org/packages/d2/40/e8714fc933d85f82c6bfc7b998a0649ad9769a32f3494ba86598aaf18a48/numpy-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ffd257026eb1b34352e749d7cc1678b5eeec3e329ad8c9965a797e08ccba205", size = 18289601 }, - { url = "https://files.pythonhosted.org/packages/80/9a/0d44b468cad50315127e884802351723daca7cf1c98d102929468c81d439/numpy-2.4.1-cp314-cp314-win32.whl", hash = "sha256:727c6c3275ddefa0dc078524a85e064c057b4f4e71ca5ca29a19163c607be745", size = 6005722 }, - { url = "https://files.pythonhosted.org/packages/7e/bb/c6513edcce5a831810e2dddc0d3452ce84d208af92405a0c2e58fd8e7881/numpy-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:7d5d7999df434a038d75a748275cd6c0094b0ecdb0837342b332a82defc4dc4d", size = 12438590 }, - { url = "https://files.pythonhosted.org/packages/e9/da/a598d5cb260780cf4d255102deba35c1d072dc028c4547832f45dd3323a8/numpy-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:ce9ce141a505053b3c7bce3216071f3bf5c182b8b28930f14cd24d43932cd2df", size = 10596180 }, - { url = "https://files.pythonhosted.org/packages/de/bc/ea3f2c96fcb382311827231f911723aeff596364eb6e1b6d1d91128aa29b/numpy-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e53170557d37ae404bf8d542ca5b7c629d6efa1117dac6a83e394142ea0a43f", size = 12498774 }, - { url = "https://files.pythonhosted.org/packages/aa/ab/ef9d939fe4a812648c7a712610b2ca6140b0853c5efea361301006c02ae5/numpy-2.4.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:a73044b752f5d34d4232f25f18160a1cc418ea4507f5f11e299d8ac36875f8a0", size = 5327274 }, - { url = "https://files.pythonhosted.org/packages/bd/31/d381368e2a95c3b08b8cf7faac6004849e960f4a042d920337f71cef0cae/numpy-2.4.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:fb1461c99de4d040666ca0444057b06541e5642f800b71c56e6ea92d6a853a0c", size = 6648306 }, - { url = "https://files.pythonhosted.org/packages/c8/e5/0989b44ade47430be6323d05c23207636d67d7362a1796ccbccac6773dd2/numpy-2.4.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423797bdab2eeefbe608d7c1ec7b2b4fd3c58d51460f1ee26c7500a1d9c9ee93", size = 14464653 }, - { url = "https://files.pythonhosted.org/packages/10/a7/cfbe475c35371cae1358e61f20c5f075badc18c4797ab4354140e1d283cf/numpy-2.4.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52b5f61bdb323b566b528899cc7db2ba5d1015bda7ea811a8bcf3c89c331fa42", size = 16405144 }, - { url = "https://files.pythonhosted.org/packages/f8/a3/0c63fe66b534888fa5177cc7cef061541064dbe2b4b60dcc60ffaf0d2157/numpy-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:42d7dd5fa36d16d52a84f821eb96031836fd405ee6955dd732f2023724d0aa01", size = 16247425 }, - { url = "https://files.pythonhosted.org/packages/6b/2b/55d980cfa2c93bd40ff4c290bf824d792bd41d2fe3487b07707559071760/numpy-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b6b5e28bbd47b7532698e5db2fe1db693d84b58c254e4389d99a27bb9b8f6b", size = 18330053 }, - { url = "https://files.pythonhosted.org/packages/23/12/8b5fc6b9c487a09a7957188e0943c9ff08432c65e34567cabc1623b03a51/numpy-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:5de60946f14ebe15e713a6f22850c2372fa72f4ff9a432ab44aa90edcadaa65a", size = 6152482 }, - { url = "https://files.pythonhosted.org/packages/00/a5/9f8ca5856b8940492fc24fbe13c1bc34d65ddf4079097cf9e53164d094e1/numpy-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8f085da926c0d491ffff3096f91078cc97ea67e7e6b65e490bc8dcda65663be2", size = 12627117 }, - { url = "https://files.pythonhosted.org/packages/ad/0d/eca3d962f9eef265f01a8e0d20085c6dd1f443cbffc11b6dede81fd82356/numpy-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6436cffb4f2bf26c974344439439c95e152c9a527013f26b3577be6c2ca64295", size = 10667121 }, -] - -[[package]] -name = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 }, -] - -[[package]] -name = "ollama" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "httpx" }, - { name = "pydantic" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9d/5a/652dac4b7affc2b37b95386f8ae78f22808af09d720689e3d7a86b6ed98e/ollama-0.6.1.tar.gz", hash = "sha256:478c67546836430034b415ed64fa890fd3d1ff91781a9d548b3325274e69d7c6", size = 51620 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/4f/4a617ee93d8208d2bcf26b2d8b9402ceaed03e3853c754940e2290fed063/ollama-0.6.1-py3-none-any.whl", hash = "sha256:fc4c984b345735c5486faeee67d8a265214a31cbb828167782dc642ce0a2bf8c", size = 14354 }, -] - -[[package]] -name = "openai" -version = "2.15.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, - { name = "jiter" }, - { name = "pydantic" }, - { name = "sniffio" }, - { name = "tqdm" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/f4/4690ecb5d70023ce6bfcfeabfe717020f654bde59a775058ec6ac4692463/openai-2.15.0.tar.gz", hash = "sha256:42eb8cbb407d84770633f31bf727d4ffb4138711c670565a41663d9439174fba", size = 627383 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/df/c306f7375d42bafb379934c2df4c2fa3964656c8c782bac75ee10c102818/openai-2.15.0-py3-none-any.whl", hash = "sha256:6ae23b932cd7230f7244e52954daa6602716d6b9bf235401a107af731baea6c3", size = 1067879 }, -] - -[[package]] -name = "openai-agents" -version = "0.6.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "griffe" }, - { name = "mcp" }, - { name = "openai" }, - { name = "pydantic" }, - { name = "requests" }, - { name = "types-requests" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e7/5c/5ebface62a0efdc7298152dcd2d32164403e25e53f1088c042936d8d40f9/openai_agents-0.6.5.tar.gz", hash = "sha256:67e8cab27082d1a1fe6f3fecfcf89b41ff249988a75640bbcc2764952d603ef0", size = 2044506 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/db/16020e45d53366f2ed653ce0ddf959a647687d47180954de7654a133b910/openai_agents-0.6.5-py3-none-any.whl", hash = "sha256:c81d2eaa5c4563b8e893ba836fe170cf10ba974420ff283b4f001f84e7cb6e6b", size = 249352 }, -] - -[[package]] -name = "openai-chatkit" -version = "1.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jinja2" }, - { name = "openai" }, - { name = "openai-agents" }, - { name = "pydantic" }, - { name = "uvicorn" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/f3/3e7aafd6c29348e60d32082fb14e539661fe4100453a31b34d0fef1ff7b7/openai_chatkit-1.5.2.tar.gz", hash = "sha256:187d27b815f153fa060337c86ee3aab189f72269f23ac2bb2a35c6c88b83846d", size = 59268 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/b6/475a4c723fb2e0de30feea505505eabe77666aa7d81855d356fb289e3d8a/openai_chatkit-1.5.2-py3-none-any.whl", hash = "sha256:3bf3f140f314924ef1d4148ce5174cff6aa4c5d1760f988ba2aa267fd434f960", size = 41482 }, -] - -[[package]] -name = "opentelemetry-api" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356 }, -] - -[[package]] -name = "opentelemetry-sdk" -version = "1.39.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "opentelemetry-semantic-conventions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565 }, -] - -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.60b1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "opentelemetry-api" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982 }, -] - -[[package]] -name = "opentelemetry-semantic-conventions-ai" -version = "0.4.13" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e6/40b59eda51ac47009fb47afcdf37c6938594a0bd7f3b9fadcbc6058248e3/opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036", size = 5368 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080 }, -] - -[[package]] -name = "orderedmultidict" -version = "1.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/62/61ad51f6c19d495970230a7747147ce7ed3c3a63c2af4ebfdb1f6d738703/orderedmultidict-1.0.2.tar.gz", hash = "sha256:16a7ae8432e02cc987d2d6d5af2df5938258f87c870675c73ee77a0920e6f4a6", size = 13973 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/6c/d8a02ffb24876b5f51fbd781f479fc6525a518553a4196bd0433dae9ff8e/orderedmultidict-1.0.2-py2.py3-none-any.whl", hash = "sha256:ab5044c1dca4226ae4c28524cfc5cc4c939f0b49e978efa46a6ad6468049f79b", size = 11897 }, -] - -[[package]] -name = "packaging" -version = "26.0rc2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2d/29/b1656a8724cb5d53eb011bdb8747ade15e6a875d23a1b99bba09cd8db264/packaging-26.0rc2.tar.gz", hash = "sha256:51c9779f69ab1f6ed1a4d6d0e2f42e2e64b566955a5eff1f7f83bcab688035a4", size = 142648 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/eb/1f8f5e3b10748612b075b2b991d6c4342d993008d2aa05f5c872a4e7bfa5/packaging-26.0rc2-py3-none-any.whl", hash = "sha256:885e01b9dbe4913e5080fa516b8550d43ef38549088c63e6e8bb51cd25adea4a", size = 74124 }, -] - -[[package]] -name = "pandas" -version = "2.3.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "python-dateutil" }, - { name = "pytz" }, - { name = "tzdata" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846 }, - { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618 }, - { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212 }, - { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693 }, - { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002 }, - { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971 }, - { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722 }, - { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671 }, - { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807 }, - { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872 }, - { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371 }, - { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333 }, - { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120 }, - { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991 }, - { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227 }, - { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056 }, - { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189 }, - { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912 }, - { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160 }, - { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233 }, - { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635 }, - { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079 }, - { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049 }, - { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638 }, - { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834 }, - { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925 }, - { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071 }, - { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504 }, - { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702 }, - { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535 }, - { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582 }, - { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963 }, - { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175 }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, -] - -[[package]] -name = "ply" -version = "3.11" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567 }, -] - -[[package]] -name = "portalocker" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424 }, -] - -[[package]] -name = "posthog" -version = "7.5.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "backoff" }, - { name = "distro" }, - { name = "python-dateutil" }, - { name = "requests" }, - { name = "six" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/98/3b/866af11cb12e9d35feffcd480d4ebf31f87b2164926b9c670cbdafabc814/posthog-7.5.1.tar.gz", hash = "sha256:d8a8165b3d47465023ea2f919982a34890e2dda76402ec47d6c68424b2534a55", size = 145244 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/03/ba011712ce9d07fe87dcfb72474c388d960e6d0c4f2262d2ae11fd27f0c5/posthog-7.5.1-py3-none-any.whl", hash = "sha256:fd3431ce32c9bbfb1e3775e3633c32ee589c052b0054fafe5ed9e4b17c1969d3", size = 167555 }, -] - -[[package]] -name = "powerfx" -version = "0.0.34" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "python_full_version < '3.14'" }, - { name = "pythonnet", marker = "python_full_version < '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9f/fb/6c4bf87e0c74ca1c563921ce89ca1c5785b7576bca932f7255cdf81082a7/powerfx-0.0.34.tar.gz", hash = "sha256:956992e7afd272657ed16d80f4cad24ec95d9e4a79fb9dfa4a068a09e136af32", size = 3237555 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/96/0f8a1f86485b3ec0315e3e8403326884a0334b3dcd699df2482669cca4be/powerfx-0.0.34-py3-none-any.whl", hash = "sha256:f2dc1c42ba8bfa4c72a7fcff2a00755b95394547388ca0b3e36579c49ee7ed75", size = 3483089 }, -] - -[[package]] -name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061 }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037 }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324 }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505 }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242 }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474 }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575 }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736 }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019 }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376 }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988 }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615 }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066 }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655 }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789 }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750 }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780 }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308 }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182 }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215 }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112 }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442 }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398 }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920 }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748 }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877 }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437 }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586 }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790 }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158 }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451 }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374 }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396 }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950 }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856 }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420 }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254 }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205 }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873 }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739 }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514 }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781 }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396 }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897 }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789 }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152 }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869 }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596 }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981 }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490 }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371 }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424 }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566 }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130 }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625 }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209 }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797 }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140 }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257 }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097 }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455 }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372 }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411 }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712 }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557 }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015 }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880 }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938 }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641 }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510 }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161 }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393 }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546 }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259 }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428 }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305 }, -] - -[[package]] -name = "proto-plus" -version = "1.27.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "protobuf" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/01/89/9cbe2f4bba860e149108b683bc2efec21f14d5f7ed6e25562ad86acbc373/proto_plus-1.27.0.tar.gz", hash = "sha256:873af56dd0d7e91836aee871e5799e1c6f1bda86ac9a983e0bb9f0c266a568c4", size = 56158 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/24/3b7a0818484df9c28172857af32c2397b6d8fcd99d9468bd4684f98ebf0a/proto_plus-1.27.0-py3-none-any.whl", hash = "sha256:1baa7f81cf0f8acb8bc1f6d085008ba4171eaf669629d1b6d1673b21ed1c0a82", size = 50205 }, -] - -[[package]] -name = "protobuf" -version = "5.29.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/29/d09e70352e4e88c9c7a198d5645d7277811448d76c23b00345670f7c8a38/protobuf-5.29.5.tar.gz", hash = "sha256:bc1463bafd4b0929216c35f437a8e28731a2b7fe3d98bb77a600efced5a15c84", size = 425226 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/11/6e40e9fc5bba02988a214c07cf324595789ca7820160bfd1f8be96e48539/protobuf-5.29.5-cp310-abi3-win32.whl", hash = "sha256:3f1c6468a2cfd102ff4703976138844f78ebd1fb45f49011afc5139e9e283079", size = 422963 }, - { url = "https://files.pythonhosted.org/packages/81/7f/73cefb093e1a2a7c3ffd839e6f9fcafb7a427d300c7f8aef9c64405d8ac6/protobuf-5.29.5-cp310-abi3-win_amd64.whl", hash = "sha256:3f76e3a3675b4a4d867b52e4a5f5b78a2ef9565549d4037e06cf7b0942b1d3fc", size = 434818 }, - { url = "https://files.pythonhosted.org/packages/dd/73/10e1661c21f139f2c6ad9b23040ff36fee624310dc28fba20d33fdae124c/protobuf-5.29.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e38c5add5a311f2a6eb0340716ef9b039c1dfa428b28f25a7838ac329204a671", size = 418091 }, - { url = "https://files.pythonhosted.org/packages/6c/04/98f6f8cf5b07ab1294c13f34b4e69b3722bb609c5b701d6c169828f9f8aa/protobuf-5.29.5-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:fa18533a299d7ab6c55a238bf8629311439995f2e7eca5caaff08663606e9015", size = 319824 }, - { url = "https://files.pythonhosted.org/packages/85/e4/07c80521879c2d15f321465ac24c70efe2381378c00bf5e56a0f4fbac8cd/protobuf-5.29.5-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:63848923da3325e1bf7e9003d680ce6e14b07e55d0473253a690c3a8b8fd6e61", size = 319942 }, - { url = "https://files.pythonhosted.org/packages/7e/cc/7e77861000a0691aeea8f4566e5d3aa716f2b1dece4a24439437e41d3d25/protobuf-5.29.5-py3-none-any.whl", hash = "sha256:6cf42630262c59b2d8de33954443d94b746c952b01434fc58a417fdbd2e84bd5", size = 172823 }, -] - -[[package]] -name = "pyasn1" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, -] - -[[package]] -name = "pyasn1-modules" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, -] - -[[package]] -name = "pycparser" -version = "2.23" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140 }, -] - -[[package]] -name = "pydantic" -version = "2.12.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580 }, -] - -[[package]] -name = "pydantic-core" -version = "2.41.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, - { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, - { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, - { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, - { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, - { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, - { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, - { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, - { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, - { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, - { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, - { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, - { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, - { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, - { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, - { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, - { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, - { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, - { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, - { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, - { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, - { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, - { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, - { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, - { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, - { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, - { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, - { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, - { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, - { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, - { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, - { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, - { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, - { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, - { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, - { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, - { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, - { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, - { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, - { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, - { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, - { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, - { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, - { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, - { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, - { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, - { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, - { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, -] - -[[package]] -name = "pydantic-settings" -version = "2.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "python-dotenv" }, - { name = "typing-inspection" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/4b/ac7e0aae12027748076d72a8764ff1c9d82ca75a7a52622e67ed3f765c54/pydantic_settings-2.12.0.tar.gz", hash = "sha256:005538ef951e3c2a68e1c08b292b5f2e71490def8589d4221b95dab00dafcfd0", size = 194184 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880 }, -] - -[[package]] -name = "pygments" -version = "2.19.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, -] - -[[package]] -name = "pyjwt" -version = "2.10.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, -] - -[package.optional-dependencies] -crypto = [ - { name = "cryptography" }, -] - -[[package]] -name = "pytest" -version = "9.0.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, -] - -[[package]] -name = "pytest-asyncio" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075 }, -] - -[[package]] -name = "pytest-timeout" -version = "2.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/82/4c9ecabab13363e72d880f2fb504c5f750433b2b6f16e99f4ec21ada284c/pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a", size = 17973 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382 }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, -] - -[[package]] -name = "python-dotenv" -version = "1.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230 }, -] - -[[package]] -name = "python-multipart" -version = "0.0.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/78/96/804520d0850c7db98e5ccb70282e29208723f0964e88ffd9d0da2f52ea09/python_multipart-0.0.21.tar.gz", hash = "sha256:7137ebd4d3bbf70ea1622998f902b97a29434a9e8dc40eb203bbcf7c2a2cba92", size = 37196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/76/03af049af4dcee5d27442f71b6924f01f3efb5d2bd34f23fcd563f2cc5f5/python_multipart-0.0.21-py3-none-any.whl", hash = "sha256:cf7a6713e01c87aa35387f4774e812c4361150938d20d232800f75ffcf266090", size = 24541 }, -] - -[[package]] -name = "python-ulid" -version = "3.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/7e/0d6c82b5ccc71e7c833aed43d9e8468e1f2ff0be1b3f657a6fcafbb8433d/python_ulid-3.1.0.tar.gz", hash = "sha256:ff0410a598bc5f6b01b602851a3296ede6f91389f913a5d5f8c496003836f636", size = 93175 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6c/a0/4ed6632b70a52de845df056654162acdebaf97c20e3212c559ac43e7216e/python_ulid-3.1.0-py3-none-any.whl", hash = "sha256:e2cdc979c8c877029b4b7a38a6fba3bc4578e4f109a308419ff4d3ccf0a46619", size = 11577 }, -] - -[[package]] -name = "pythonnet" -version = "3.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "clr-loader", marker = "python_full_version < '3.14'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9a/d6/1afd75edd932306ae9bd2c2d961d603dc2b52fcec51b04afea464f1f6646/pythonnet-3.0.5.tar.gz", hash = "sha256:48e43ca463941b3608b32b4e236db92d8d40db4c58a75ace902985f76dac21cf", size = 239212 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/f1/bfb6811df4745f92f14c47a29e50e89a36b1533130fcc56452d4660bd2d6/pythonnet-3.0.5-py3-none-any.whl", hash = "sha256:f6702d694d5d5b163c9f3f5cc34e0bed8d6857150237fae411fefb883a656d20", size = 297506 }, -] - -[[package]] -name = "pytz" -version = "2025.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, -] - -[[package]] -name = "pywin32" -version = "311" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543 }, - { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040 }, - { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102 }, - { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700 }, - { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700 }, - { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318 }, - { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714 }, - { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800 }, - { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540 }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, -] - -[[package]] -name = "qdrant-client" -version = "1.16.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "httpx", extra = ["http2"] }, - { name = "numpy" }, - { name = "portalocker" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/7d/3cd10e26ae97b35cf856ca1dc67576e42414ae39502c51165bb36bb1dff8/qdrant_client-1.16.2.tar.gz", hash = "sha256:ca4ef5f9be7b5eadeec89a085d96d5c723585a391eb8b2be8192919ab63185f0", size = 331112 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/08/13/8ce16f808297e16968269de44a14f4fef19b64d9766be1d6ba5ba78b579d/qdrant_client-1.16.2-py3-none-any.whl", hash = "sha256:442c7ef32ae0f005e88b5d3c0783c63d4912b97ae756eb5e052523be682f17d3", size = 377186 }, -] - -[[package]] -name = "redis" -version = "7.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159 }, -] - -[[package]] -name = "redisvl" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonpath-ng" }, - { name = "ml-dtypes" }, - { name = "numpy" }, - { name = "pydantic" }, - { name = "python-ulid" }, - { name = "pyyaml" }, - { name = "redis" }, - { name = "tenacity" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/81/d6/8f3235b272e3a2370698d7524aad2dec15f53c5be5d6726ba41056844f69/redisvl-0.13.2.tar.gz", hash = "sha256:f34c4350922ac469c45d90b5db65c49950e6aa8706331931b000f631ff9a0f4a", size = 737736 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/93/81ea5c45637ce7fe2fdaf214d5e1b91afe96a472edeb9b659e24d3710dfb/redisvl-0.13.2-py3-none-any.whl", hash = "sha256:dd998c6acc54f13526d464ad6b6e6f0c4cf6985fb2c7a1655bdf8ed8e57a4c01", size = 192760 }, -] - -[[package]] -name = "referencing" -version = "0.37.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "rpds-py" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766 }, -] - -[[package]] -name = "regex" -version = "2025.11.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cc/a9/546676f25e573a4cf00fe8e119b78a37b6a8fe2dc95cda877b30889c9c45/regex-2025.11.3.tar.gz", hash = "sha256:1fedc720f9bb2494ce31a58a1631f9c82df6a09b49c19517ea5cc280b4541e01", size = 414669 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312 }, - { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256 }, - { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921 }, - { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568 }, - { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165 }, - { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182 }, - { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501 }, - { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842 }, - { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519 }, - { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611 }, - { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759 }, - { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194 }, - { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069 }, - { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330 }, - { url = "https://files.pythonhosted.org/packages/e1/a7/dda24ebd49da46a197436ad96378f17df30ceb40e52e859fc42cac45b850/regex-2025.11.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c1e448051717a334891f2b9a620fe36776ebf3dd8ec46a0b877c8ae69575feb4", size = 489081 }, - { url = "https://files.pythonhosted.org/packages/19/22/af2dc751aacf88089836aa088a1a11c4f21a04707eb1b0478e8e8fb32847/regex-2025.11.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b5aca4d5dfd7fbfbfbdaf44850fcc7709a01146a797536a8f84952e940cca76", size = 291123 }, - { url = "https://files.pythonhosted.org/packages/a3/88/1a3ea5672f4b0a84802ee9891b86743438e7c04eb0b8f8c4e16a42375327/regex-2025.11.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:04d2765516395cf7dda331a244a3282c0f5ae96075f728629287dfa6f76ba70a", size = 288814 }, - { url = "https://files.pythonhosted.org/packages/fb/8c/f5987895bf42b8ddeea1b315c9fedcfe07cadee28b9c98cf50d00adcb14d/regex-2025.11.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d9903ca42bfeec4cebedba8022a7c97ad2aab22e09573ce9976ba01b65e4361", size = 798592 }, - { url = "https://files.pythonhosted.org/packages/99/2a/6591ebeede78203fa77ee46a1c36649e02df9eaa77a033d1ccdf2fcd5d4e/regex-2025.11.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:639431bdc89d6429f6721625e8129413980ccd62e9d3f496be618a41d205f160", size = 864122 }, - { url = "https://files.pythonhosted.org/packages/94/d6/be32a87cf28cf8ed064ff281cfbd49aefd90242a83e4b08b5a86b38e8eb4/regex-2025.11.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f117efad42068f9715677c8523ed2be1518116d1c49b1dd17987716695181efe", size = 912272 }, - { url = "https://files.pythonhosted.org/packages/62/11/9bcef2d1445665b180ac7f230406ad80671f0fc2a6ffb93493b5dd8cd64c/regex-2025.11.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4aecb6f461316adf9f1f0f6a4a1a3d79e045f9b71ec76055a791affa3b285850", size = 803497 }, - { url = "https://files.pythonhosted.org/packages/e5/a7/da0dc273d57f560399aa16d8a68ae7f9b57679476fc7ace46501d455fe84/regex-2025.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3b3a5f320136873cc5561098dfab677eea139521cb9a9e8db98b7e64aef44cbc", size = 787892 }, - { url = "https://files.pythonhosted.org/packages/da/4b/732a0c5a9736a0b8d6d720d4945a2f1e6f38f87f48f3173559f53e8d5d82/regex-2025.11.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:75fa6f0056e7efb1f42a1c34e58be24072cb9e61a601340cc1196ae92326a4f9", size = 858462 }, - { url = "https://files.pythonhosted.org/packages/0c/f5/a2a03df27dc4c2d0c769220f5110ba8c4084b0bfa9ab0f9b4fcfa3d2b0fc/regex-2025.11.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:dbe6095001465294f13f1adcd3311e50dd84e5a71525f20a10bd16689c61ce0b", size = 850528 }, - { url = "https://files.pythonhosted.org/packages/d6/09/e1cd5bee3841c7f6eb37d95ca91cdee7100b8f88b81e41c2ef426910891a/regex-2025.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:454d9b4ae7881afbc25015b8627c16d88a597479b9dea82b8c6e7e2e07240dc7", size = 789866 }, - { url = "https://files.pythonhosted.org/packages/eb/51/702f5ea74e2a9c13d855a6a85b7f80c30f9e72a95493260193c07f3f8d74/regex-2025.11.3-cp313-cp313-win32.whl", hash = "sha256:28ba4d69171fc6e9896337d4fc63a43660002b7da53fc15ac992abcf3410917c", size = 266189 }, - { url = "https://files.pythonhosted.org/packages/8b/00/6e29bb314e271a743170e53649db0fdb8e8ff0b64b4f425f5602f4eb9014/regex-2025.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:bac4200befe50c670c405dc33af26dad5a3b6b255dd6c000d92fe4629f9ed6a5", size = 277054 }, - { url = "https://files.pythonhosted.org/packages/25/f1/b156ff9f2ec9ac441710764dda95e4edaf5f36aca48246d1eea3f1fd96ec/regex-2025.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:2292cd5a90dab247f9abe892ac584cb24f0f54680c73fcb4a7493c66c2bf2467", size = 270325 }, - { url = "https://files.pythonhosted.org/packages/20/28/fd0c63357caefe5680b8ea052131acbd7f456893b69cc2a90cc3e0dc90d4/regex-2025.11.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:1eb1ebf6822b756c723e09f5186473d93236c06c579d2cc0671a722d2ab14281", size = 491984 }, - { url = "https://files.pythonhosted.org/packages/df/ec/7014c15626ab46b902b3bcc4b28a7bae46d8f281fc7ea9c95e22fcaaa917/regex-2025.11.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1e00ec2970aab10dc5db34af535f21fcf32b4a31d99e34963419636e2f85ae39", size = 292673 }, - { url = "https://files.pythonhosted.org/packages/23/ab/3b952ff7239f20d05f1f99e9e20188513905f218c81d52fb5e78d2bf7634/regex-2025.11.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a4cb042b615245d5ff9b3794f56be4138b5adc35a4166014d31d1814744148c7", size = 291029 }, - { url = "https://files.pythonhosted.org/packages/21/7e/3dc2749fc684f455f162dcafb8a187b559e2614f3826877d3844a131f37b/regex-2025.11.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44f264d4bf02f3176467d90b294d59bf1db9fe53c141ff772f27a8b456b2a9ed", size = 807437 }, - { url = "https://files.pythonhosted.org/packages/1b/0b/d529a85ab349c6a25d1ca783235b6e3eedf187247eab536797021f7126c6/regex-2025.11.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7be0277469bf3bd7a34a9c57c1b6a724532a0d235cd0dc4e7f4316f982c28b19", size = 873368 }, - { url = "https://files.pythonhosted.org/packages/7d/18/2d868155f8c9e3e9d8f9e10c64e9a9f496bb8f7e037a88a8bed26b435af6/regex-2025.11.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0d31e08426ff4b5b650f68839f5af51a92a5b51abd8554a60c2fbc7c71f25d0b", size = 914921 }, - { url = "https://files.pythonhosted.org/packages/2d/71/9d72ff0f354fa783fe2ba913c8734c3b433b86406117a8db4ea2bf1c7a2f/regex-2025.11.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e43586ce5bd28f9f285a6e729466841368c4a0353f6fd08d4ce4630843d3648a", size = 812708 }, - { url = "https://files.pythonhosted.org/packages/e7/19/ce4bf7f5575c97f82b6e804ffb5c4e940c62609ab2a0d9538d47a7fdf7d4/regex-2025.11.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0f9397d561a4c16829d4e6ff75202c1c08b68a3bdbfe29dbfcdb31c9830907c6", size = 795472 }, - { url = "https://files.pythonhosted.org/packages/03/86/fd1063a176ffb7b2315f9a1b08d17b18118b28d9df163132615b835a26ee/regex-2025.11.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:dd16e78eb18ffdb25ee33a0682d17912e8cc8a770e885aeee95020046128f1ce", size = 868341 }, - { url = "https://files.pythonhosted.org/packages/12/43/103fb2e9811205e7386366501bc866a164a0430c79dd59eac886a2822950/regex-2025.11.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:ffcca5b9efe948ba0661e9df0fa50d2bc4b097c70b9810212d6b62f05d83b2dd", size = 854666 }, - { url = "https://files.pythonhosted.org/packages/7d/22/e392e53f3869b75804762c7c848bd2dd2abf2b70fb0e526f58724638bd35/regex-2025.11.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c56b4d162ca2b43318ac671c65bd4d563e841a694ac70e1a976ac38fcf4ca1d2", size = 799473 }, - { url = "https://files.pythonhosted.org/packages/4f/f9/8bd6b656592f925b6845fcbb4d57603a3ac2fb2373344ffa1ed70aa6820a/regex-2025.11.3-cp313-cp313t-win32.whl", hash = "sha256:9ddc42e68114e161e51e272f667d640f97e84a2b9ef14b7477c53aac20c2d59a", size = 268792 }, - { url = "https://files.pythonhosted.org/packages/e5/87/0e7d603467775ff65cd2aeabf1b5b50cc1c3708556a8b849a2fa4dd1542b/regex-2025.11.3-cp313-cp313t-win_amd64.whl", hash = "sha256:7a7c7fdf755032ffdd72c77e3d8096bdcb0eb92e89e17571a196f03d88b11b3c", size = 280214 }, - { url = "https://files.pythonhosted.org/packages/8d/d0/2afc6f8e94e2b64bfb738a7c2b6387ac1699f09f032d363ed9447fd2bb57/regex-2025.11.3-cp313-cp313t-win_arm64.whl", hash = "sha256:df9eb838c44f570283712e7cff14c16329a9f0fb19ca492d21d4b7528ee6821e", size = 271469 }, - { url = "https://files.pythonhosted.org/packages/31/e9/f6e13de7e0983837f7b6d238ad9458800a874bf37c264f7923e63409944c/regex-2025.11.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9697a52e57576c83139d7c6f213d64485d3df5bf84807c35fa409e6c970801c6", size = 489089 }, - { url = "https://files.pythonhosted.org/packages/a3/5c/261f4a262f1fa65141c1b74b255988bd2fa020cc599e53b080667d591cfc/regex-2025.11.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e18bc3f73bd41243c9b38a6d9f2366cd0e0137a9aebe2d8ff76c5b67d4c0a3f4", size = 291059 }, - { url = "https://files.pythonhosted.org/packages/8e/57/f14eeb7f072b0e9a5a090d1712741fd8f214ec193dba773cf5410108bb7d/regex-2025.11.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:61a08bcb0ec14ff4e0ed2044aad948d0659604f824cbd50b55e30b0ec6f09c73", size = 288900 }, - { url = "https://files.pythonhosted.org/packages/3c/6b/1d650c45e99a9b327586739d926a1cd4e94666b1bd4af90428b36af66dc7/regex-2025.11.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9c30003b9347c24bcc210958c5d167b9e4f9be786cb380a7d32f14f9b84674f", size = 799010 }, - { url = "https://files.pythonhosted.org/packages/99/ee/d66dcbc6b628ce4e3f7f0cbbb84603aa2fc0ffc878babc857726b8aab2e9/regex-2025.11.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4e1e592789704459900728d88d41a46fe3969b82ab62945560a31732ffc19a6d", size = 864893 }, - { url = "https://files.pythonhosted.org/packages/bf/2d/f238229f1caba7ac87a6c4153d79947fb0261415827ae0f77c304260c7d3/regex-2025.11.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6538241f45eb5a25aa575dbba1069ad786f68a4f2773a29a2bd3dd1f9de787be", size = 911522 }, - { url = "https://files.pythonhosted.org/packages/bd/3d/22a4eaba214a917c80e04f6025d26143690f0419511e0116508e24b11c9b/regex-2025.11.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce22519c989bb72a7e6b36a199384c53db7722fe669ba891da75907fe3587db", size = 803272 }, - { url = "https://files.pythonhosted.org/packages/84/b1/03188f634a409353a84b5ef49754b97dbcc0c0f6fd6c8ede505a8960a0a4/regex-2025.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:66d559b21d3640203ab9075797a55165d79017520685fb407b9234d72ab63c62", size = 787958 }, - { url = "https://files.pythonhosted.org/packages/99/6a/27d072f7fbf6fadd59c64d210305e1ff865cc3b78b526fd147db768c553b/regex-2025.11.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:669dcfb2e38f9e8c69507bace46f4889e3abbfd9b0c29719202883c0a603598f", size = 859289 }, - { url = "https://files.pythonhosted.org/packages/9a/70/1b3878f648e0b6abe023172dacb02157e685564853cc363d9961bcccde4e/regex-2025.11.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:32f74f35ff0f25a5021373ac61442edcb150731fbaa28286bbc8bb1582c89d02", size = 850026 }, - { url = "https://files.pythonhosted.org/packages/dd/d5/68e25559b526b8baab8e66839304ede68ff6727237a47727d240006bd0ff/regex-2025.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e6c7a21dffba883234baefe91bc3388e629779582038f75d2a5be918e250f0ed", size = 789499 }, - { url = "https://files.pythonhosted.org/packages/fc/df/43971264857140a350910d4e33df725e8c94dd9dee8d2e4729fa0d63d49e/regex-2025.11.3-cp314-cp314-win32.whl", hash = "sha256:795ea137b1d809eb6836b43748b12634291c0ed55ad50a7d72d21edf1cd565c4", size = 271604 }, - { url = "https://files.pythonhosted.org/packages/01/6f/9711b57dc6894a55faf80a4c1b5aa4f8649805cb9c7aef46f7d27e2b9206/regex-2025.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f95fbaa0ee1610ec0fc6b26668e9917a582ba80c52cc6d9ada15e30aa9ab9ad", size = 280320 }, - { url = "https://files.pythonhosted.org/packages/f1/7e/f6eaa207d4377481f5e1775cdeb5a443b5a59b392d0065f3417d31d80f87/regex-2025.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:dfec44d532be4c07088c3de2876130ff0fbeeacaa89a137decbbb5f665855a0f", size = 273372 }, - { url = "https://files.pythonhosted.org/packages/c3/06/49b198550ee0f5e4184271cee87ba4dfd9692c91ec55289e6282f0f86ccf/regex-2025.11.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ba0d8a5d7f04f73ee7d01d974d47c5834f8a1b0224390e4fe7c12a3a92a78ecc", size = 491985 }, - { url = "https://files.pythonhosted.org/packages/ce/bf/abdafade008f0b1c9da10d934034cb670432d6cf6cbe38bbb53a1cfd6cf8/regex-2025.11.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:442d86cf1cfe4faabf97db7d901ef58347efd004934da045c745e7b5bd57ac49", size = 292669 }, - { url = "https://files.pythonhosted.org/packages/f9/ef/0c357bb8edbd2ad8e273fcb9e1761bc37b8acbc6e1be050bebd6475f19c1/regex-2025.11.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fd0a5e563c756de210bb964789b5abe4f114dacae9104a47e1a649b910361536", size = 291030 }, - { url = "https://files.pythonhosted.org/packages/79/06/edbb67257596649b8fb088d6aeacbcb248ac195714b18a65e018bf4c0b50/regex-2025.11.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf3490bcbb985a1ae97b2ce9ad1c0f06a852d5b19dde9b07bdf25bf224248c95", size = 807674 }, - { url = "https://files.pythonhosted.org/packages/f4/d9/ad4deccfce0ea336296bd087f1a191543bb99ee1c53093dcd4c64d951d00/regex-2025.11.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3809988f0a8b8c9dcc0f92478d6501fac7200b9ec56aecf0ec21f4a2ec4b6009", size = 873451 }, - { url = "https://files.pythonhosted.org/packages/13/75/a55a4724c56ef13e3e04acaab29df26582f6978c000ac9cd6810ad1f341f/regex-2025.11.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f4ff94e58e84aedb9c9fce66d4ef9f27a190285b451420f297c9a09f2b9abee9", size = 914980 }, - { url = "https://files.pythonhosted.org/packages/67/1e/a1657ee15bd9116f70d4a530c736983eed997b361e20ecd8f5ca3759d5c5/regex-2025.11.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eb542fd347ce61e1321b0a6b945d5701528dca0cd9759c2e3bb8bd57e47964d", size = 812852 }, - { url = "https://files.pythonhosted.org/packages/b8/6f/f7516dde5506a588a561d296b2d0044839de06035bb486b326065b4c101e/regex-2025.11.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d6c2d5919075a1f2e413c00b056ea0c2f065b3f5fe83c3d07d325ab92dce51d6", size = 795566 }, - { url = "https://files.pythonhosted.org/packages/d9/dd/3d10b9e170cc16fb34cb2cef91513cf3df65f440b3366030631b2984a264/regex-2025.11.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3f8bf11a4827cc7ce5a53d4ef6cddd5ad25595d3c1435ef08f76825851343154", size = 868463 }, - { url = "https://files.pythonhosted.org/packages/f5/8e/935e6beff1695aa9085ff83195daccd72acc82c81793df480f34569330de/regex-2025.11.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:22c12d837298651e5550ac1d964e4ff57c3f56965fc1812c90c9fb2028eaf267", size = 854694 }, - { url = "https://files.pythonhosted.org/packages/92/12/10650181a040978b2f5720a6a74d44f841371a3d984c2083fc1752e4acf6/regex-2025.11.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:62ba394a3dda9ad41c7c780f60f6e4a70988741415ae96f6d1bf6c239cf01379", size = 799691 }, - { url = "https://files.pythonhosted.org/packages/67/90/8f37138181c9a7690e7e4cb388debbd389342db3c7381d636d2875940752/regex-2025.11.3-cp314-cp314t-win32.whl", hash = "sha256:4bf146dca15cdd53224a1bf46d628bd7590e4a07fbb69e720d561aea43a32b38", size = 274583 }, - { url = "https://files.pythonhosted.org/packages/8f/cd/867f5ec442d56beb56f5f854f40abcfc75e11d10b11fdb1869dd39c63aaf/regex-2025.11.3-cp314-cp314t-win_amd64.whl", hash = "sha256:adad1a1bcf1c9e76346e091d22d23ac54ef28e1365117d99521631078dfec9de", size = 284286 }, - { url = "https://files.pythonhosted.org/packages/20/31/32c0c4610cbc070362bf1d2e4ea86d1ea29014d400a6d6c2486fcfd57766/regex-2025.11.3-cp314-cp314t-win_arm64.whl", hash = "sha256:c54f768482cef41e219720013cd05933b6f971d9562544d691c68699bf2b6801", size = 274741 }, -] - -[[package]] -name = "requests" -version = "2.32.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, -] - -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, -] - -[[package]] -name = "rpds-py" -version = "0.30.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086 }, - { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053 }, - { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763 }, - { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951 }, - { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622 }, - { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492 }, - { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080 }, - { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680 }, - { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589 }, - { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289 }, - { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737 }, - { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120 }, - { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782 }, - { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463 }, - { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868 }, - { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887 }, - { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904 }, - { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945 }, - { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783 }, - { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021 }, - { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589 }, - { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025 }, - { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895 }, - { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799 }, - { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731 }, - { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027 }, - { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020 }, - { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139 }, - { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224 }, - { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645 }, - { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443 }, - { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375 }, - { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850 }, - { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812 }, - { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841 }, - { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149 }, - { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843 }, - { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507 }, - { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949 }, - { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790 }, - { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217 }, - { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806 }, - { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341 }, - { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768 }, - { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099 }, - { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192 }, - { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080 }, - { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841 }, - { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670 }, - { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005 }, - { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112 }, - { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049 }, - { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661 }, - { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606 }, - { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126 }, - { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371 }, - { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298 }, - { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604 }, - { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391 }, - { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868 }, - { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747 }, - { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795 }, - { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330 }, - { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194 }, - { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340 }, - { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765 }, - { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834 }, - { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470 }, - { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630 }, - { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148 }, - { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030 }, - { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570 }, - { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532 }, -] - -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 }, -] - -[[package]] -name = "ruamel-yaml" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "sqlalchemy" -version = "2.0.45" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/f9/5e4491e5ccf42f5d9cfc663741d261b3e6e1683ae7812114e7636409fcc6/sqlalchemy-2.0.45.tar.gz", hash = "sha256:1632a4bda8d2d25703fdad6363058d882541bdaaee0e5e3ddfa0cd3229efce88", size = 9869912 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/c7/1900b56ce19bff1c26f39a4ce427faec7716c81ac792bfac8b6a9f3dca93/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3ee2aac15169fb0d45822983631466d60b762085bc4535cd39e66bea362df5f", size = 3333760 }, - { url = "https://files.pythonhosted.org/packages/0a/93/3be94d96bb442d0d9a60e55a6bb6e0958dd3457751c6f8502e56ef95fed0/sqlalchemy-2.0.45-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba547ac0b361ab4f1608afbc8432db669bd0819b3e12e29fb5fa9529a8bba81d", size = 3348268 }, - { url = "https://files.pythonhosted.org/packages/48/4b/f88ded696e61513595e4a9778f9d3f2bf7332cce4eb0c7cedaabddd6687b/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:215f0528b914e5c75ef2559f69dca86878a3beeb0c1be7279d77f18e8d180ed4", size = 3278144 }, - { url = "https://files.pythonhosted.org/packages/ed/6a/310ecb5657221f3e1bd5288ed83aa554923fb5da48d760a9f7622afeb065/sqlalchemy-2.0.45-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:107029bf4f43d076d4011f1afb74f7c3e2ea029ec82eb23d8527d5e909e97aa6", size = 3313907 }, - { url = "https://files.pythonhosted.org/packages/5c/39/69c0b4051079addd57c84a5bfb34920d87456dd4c90cf7ee0df6efafc8ff/sqlalchemy-2.0.45-cp312-cp312-win32.whl", hash = "sha256:0c9f6ada57b58420a2c0277ff853abe40b9e9449f8d7d231763c6bc30f5c4953", size = 2112182 }, - { url = "https://files.pythonhosted.org/packages/f7/4e/510db49dd89fc3a6e994bee51848c94c48c4a00dc905e8d0133c251f41a7/sqlalchemy-2.0.45-cp312-cp312-win_amd64.whl", hash = "sha256:8defe5737c6d2179c7997242d6473587c3beb52e557f5ef0187277009f73e5e1", size = 2139200 }, - { url = "https://files.pythonhosted.org/packages/6a/c8/7cc5221b47a54edc72a0140a1efa56e0a2730eefa4058d7ed0b4c4357ff8/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe187fc31a54d7fd90352f34e8c008cf3ad5d064d08fedd3de2e8df83eb4a1cf", size = 3277082 }, - { url = "https://files.pythonhosted.org/packages/0e/50/80a8d080ac7d3d321e5e5d420c9a522b0aa770ec7013ea91f9a8b7d36e4a/sqlalchemy-2.0.45-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:672c45cae53ba88e0dad74b9027dddd09ef6f441e927786b05bec75d949fbb2e", size = 3293131 }, - { url = "https://files.pythonhosted.org/packages/da/4c/13dab31266fc9904f7609a5dc308a2432a066141d65b857760c3bef97e69/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:470daea2c1ce73910f08caf10575676a37159a6d16c4da33d0033546bddebc9b", size = 3225389 }, - { url = "https://files.pythonhosted.org/packages/74/04/891b5c2e9f83589de202e7abaf24cd4e4fa59e1837d64d528829ad6cc107/sqlalchemy-2.0.45-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9c6378449e0940476577047150fd09e242529b761dc887c9808a9a937fe990c8", size = 3266054 }, - { url = "https://files.pythonhosted.org/packages/f1/24/fc59e7f71b0948cdd4cff7a286210e86b0443ef1d18a23b0d83b87e4b1f7/sqlalchemy-2.0.45-cp313-cp313-win32.whl", hash = "sha256:4b6bec67ca45bc166c8729910bd2a87f1c0407ee955df110d78948f5b5827e8a", size = 2110299 }, - { url = "https://files.pythonhosted.org/packages/c0/c5/d17113020b2d43073412aeca09b60d2009442420372123b8d49cc253f8b8/sqlalchemy-2.0.45-cp313-cp313-win_amd64.whl", hash = "sha256:afbf47dc4de31fa38fd491f3705cac5307d21d4bb828a4f020ee59af412744ee", size = 2136264 }, - { url = "https://files.pythonhosted.org/packages/3d/8d/bb40a5d10e7a5f2195f235c0b2f2c79b0bf6e8f00c0c223130a4fbd2db09/sqlalchemy-2.0.45-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d7009f40ce619d483d26ac1b757dfe3167b39921379a8bd1b596cf02dab4a6", size = 3521998 }, - { url = "https://files.pythonhosted.org/packages/75/a5/346128b0464886f036c039ea287b7332a410aa2d3fb0bb5d404cb8861635/sqlalchemy-2.0.45-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d8a2ca754e5415cde2b656c27900b19d50ba076aa05ce66e2207623d3fe41f5a", size = 3473434 }, - { url = "https://files.pythonhosted.org/packages/cc/64/4e1913772646b060b025d3fc52ce91a58967fe58957df32b455de5a12b4f/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f46ec744e7f51275582e6a24326e10c49fbdd3fc99103e01376841213028774", size = 3272404 }, - { url = "https://files.pythonhosted.org/packages/b3/27/caf606ee924282fe4747ee4fd454b335a72a6e018f97eab5ff7f28199e16/sqlalchemy-2.0.45-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:883c600c345123c033c2f6caca18def08f1f7f4c3ebeb591a63b6fceffc95cce", size = 3277057 }, - { url = "https://files.pythonhosted.org/packages/85/d0/3d64218c9724e91f3d1574d12eb7ff8f19f937643815d8daf792046d88ab/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2c0b74aa79e2deade948fe8593654c8ef4228c44ba862bb7c9585c8e0db90f33", size = 3222279 }, - { url = "https://files.pythonhosted.org/packages/24/10/dd7688a81c5bc7690c2a3764d55a238c524cd1a5a19487928844cb247695/sqlalchemy-2.0.45-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8a420169cef179d4c9064365f42d779f1e5895ad26ca0c8b4c0233920973db74", size = 3244508 }, - { url = "https://files.pythonhosted.org/packages/aa/41/db75756ca49f777e029968d9c9fee338c7907c563267740c6d310a8e3f60/sqlalchemy-2.0.45-cp314-cp314-win32.whl", hash = "sha256:e50dcb81a5dfe4b7b4a4aa8f338116d127cb209559124f3694c70d6cd072b68f", size = 2113204 }, - { url = "https://files.pythonhosted.org/packages/89/a2/0e1590e9adb292b1d576dbcf67ff7df8cf55e56e78d2c927686d01080f4b/sqlalchemy-2.0.45-cp314-cp314-win_amd64.whl", hash = "sha256:4748601c8ea959e37e03d13dcda4a44837afcd1b21338e637f7c935b8da06177", size = 2138785 }, - { url = "https://files.pythonhosted.org/packages/42/39/f05f0ed54d451156bbed0e23eb0516bcad7cbb9f18b3bf219c786371b3f0/sqlalchemy-2.0.45-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd337d3526ec5298f67d6a30bbbe4ed7e5e68862f0bf6dd21d289f8d37b7d60b", size = 3522029 }, - { url = "https://files.pythonhosted.org/packages/54/0f/d15398b98b65c2bce288d5ee3f7d0a81f77ab89d9456994d5c7cc8b2a9db/sqlalchemy-2.0.45-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9a62b446b7d86a3909abbcd1cd3cc550a832f99c2bc37c5b22e1925438b9367b", size = 3475142 }, - { url = "https://files.pythonhosted.org/packages/bf/e1/3ccb13c643399d22289c6a9786c1a91e3dcbb68bce4beb44926ac2c557bf/sqlalchemy-2.0.45-py3-none-any.whl", hash = "sha256:5225a288e4c8cc2308dbdd874edad6e7d0fd38eac1e9e5f23503425c8eee20d0", size = 1936672 }, -] - -[[package]] -name = "sse-starlette" -version = "3.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "starlette" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/34/f5df66cb383efdbf4f2db23cabb27f51b1dcb737efaf8a558f6f1d195134/sse_starlette-3.1.2.tar.gz", hash = "sha256:55eff034207a83a0eb86de9a68099bd0157838f0b8b999a1b742005c71e33618", size = 26303 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/95/8c4b76eec9ae574474e5d2997557cebf764bcd3586458956c30631ae08f4/sse_starlette-3.1.2-py3-none-any.whl", hash = "sha256:cd800dd349f4521b317b9391d3796fa97b71748a4da9b9e00aafab32dda375c8", size = 12484 }, -] - -[[package]] -name = "starlette" -version = "0.50.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033 }, -] - -[[package]] -name = "tenacity" -version = "9.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 }, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540 }, -] - -[[package]] -name = "types-requests" -version = "2.32.4.20260107" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676 }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, -] - -[[package]] -name = "typing-inspection" -version = "0.4.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, -] - -[[package]] -name = "tzdata" -version = "2025.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 }, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, -] - -[[package]] -name = "uvicorn" -version = "0.40.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502 }, -] - -[package.optional-dependencies] -standard = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "httptools" }, - { name = "python-dotenv" }, - { name = "pyyaml" }, - { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, - { name = "watchfiles" }, - { name = "websockets" }, -] - -[[package]] -name = "uvloop" -version = "0.22.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936 }, - { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769 }, - { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413 }, - { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307 }, - { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970 }, - { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343 }, - { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611 }, - { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811 }, - { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562 }, - { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890 }, - { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472 }, - { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051 }, - { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067 }, - { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423 }, - { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437 }, - { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101 }, - { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360 }, - { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790 }, - { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783 }, - { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548 }, - { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065 }, - { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384 }, - { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730 }, -] - -[[package]] -name = "watchfiles" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745 }, - { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769 }, - { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374 }, - { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485 }, - { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813 }, - { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816 }, - { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186 }, - { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812 }, - { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196 }, - { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657 }, - { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042 }, - { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410 }, - { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209 }, - { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321 }, - { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783 }, - { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279 }, - { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405 }, - { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976 }, - { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506 }, - { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936 }, - { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147 }, - { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007 }, - { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280 }, - { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056 }, - { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162 }, - { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909 }, - { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389 }, - { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964 }, - { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114 }, - { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264 }, - { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877 }, - { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176 }, - { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577 }, - { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425 }, - { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826 }, - { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208 }, - { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315 }, - { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869 }, - { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919 }, - { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845 }, - { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027 }, - { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615 }, - { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836 }, - { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099 }, - { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626 }, - { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519 }, - { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078 }, - { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664 }, - { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154 }, - { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820 }, - { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510 }, - { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408 }, - { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968 }, - { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096 }, - { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040 }, - { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847 }, - { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072 }, - { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104 }, - { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112 }, -] - -[[package]] -name = "websockets" -version = "16.0" -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 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261 }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693 }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255 }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689 }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915 }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381 }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085 }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531 }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598 }, -] - -[[package]] -name = "werkzeug" -version = "3.1.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025 }, -] - -[[package]] -name = "yarl" -version = "1.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "multidict" }, - { name = "propcache" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/57/63/0c6ebca57330cd313f6102b16dd57ffaf3ec4c83403dcb45dbd15c6f3ea1/yarl-1.22.0.tar.gz", hash = "sha256:bebf8557577d4401ba8bd9ff33906f1376c877aa78d1fe216ad01b4d6745af71", size = 187169 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/75/ff/46736024fee3429b80a165a732e38e5d5a238721e634ab41b040d49f8738/yarl-1.22.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e340382d1afa5d32b892b3ff062436d592ec3d692aeea3bef3a5cfe11bbf8c6f", size = 142000 }, - { url = "https://files.pythonhosted.org/packages/5a/9a/b312ed670df903145598914770eb12de1bac44599549b3360acc96878df8/yarl-1.22.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f1e09112a2c31ffe8d80be1b0988fa6a18c5d5cad92a9ffbb1c04c91bfe52ad2", size = 94338 }, - { url = "https://files.pythonhosted.org/packages/ba/f5/0601483296f09c3c65e303d60c070a5c19fcdbc72daa061e96170785bc7d/yarl-1.22.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:939fe60db294c786f6b7c2d2e121576628468f65453d86b0fe36cb52f987bd74", size = 94909 }, - { url = "https://files.pythonhosted.org/packages/60/41/9a1fe0b73dbcefce72e46cf149b0e0a67612d60bfc90fb59c2b2efdfbd86/yarl-1.22.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1651bf8e0398574646744c1885a41198eba53dc8a9312b954073f845c90a8df", size = 372940 }, - { url = "https://files.pythonhosted.org/packages/17/7a/795cb6dfee561961c30b800f0ed616b923a2ec6258b5def2a00bf8231334/yarl-1.22.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b8a0588521a26bf92a57a1705b77b8b59044cdceccac7151bd8d229e66b8dedb", size = 345825 }, - { url = "https://files.pythonhosted.org/packages/d7/93/a58f4d596d2be2ae7bab1a5846c4d270b894958845753b2c606d666744d3/yarl-1.22.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42188e6a615c1a75bcaa6e150c3fe8f3e8680471a6b10150c5f7e83f47cc34d2", size = 386705 }, - { url = "https://files.pythonhosted.org/packages/61/92/682279d0e099d0e14d7fd2e176bd04f48de1484f56546a3e1313cd6c8e7c/yarl-1.22.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6d2cb59377d99718913ad9a151030d6f83ef420a2b8f521d94609ecc106ee82", size = 396518 }, - { url = "https://files.pythonhosted.org/packages/db/0f/0d52c98b8a885aeda831224b78f3be7ec2e1aa4a62091f9f9188c3c65b56/yarl-1.22.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50678a3b71c751d58d7908edc96d332af328839eea883bb554a43f539101277a", size = 377267 }, - { url = "https://files.pythonhosted.org/packages/22/42/d2685e35908cbeaa6532c1fc73e89e7f2efb5d8a7df3959ea8e37177c5a3/yarl-1.22.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1e8fbaa7cec507aa24ea27a01456e8dd4b6fab829059b69844bd348f2d467124", size = 365797 }, - { url = "https://files.pythonhosted.org/packages/a2/83/cf8c7bcc6355631762f7d8bdab920ad09b82efa6b722999dfb05afa6cfac/yarl-1.22.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:433885ab5431bc3d3d4f2f9bd15bfa1614c522b0f1405d62c4f926ccd69d04fa", size = 365535 }, - { url = "https://files.pythonhosted.org/packages/25/e1/5302ff9b28f0c59cac913b91fe3f16c59a033887e57ce9ca5d41a3a94737/yarl-1.22.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b790b39c7e9a4192dc2e201a282109ed2985a1ddbd5ac08dc56d0e121400a8f7", size = 382324 }, - { url = "https://files.pythonhosted.org/packages/bf/cd/4617eb60f032f19ae3a688dc990d8f0d89ee0ea378b61cac81ede3e52fae/yarl-1.22.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31f0b53913220599446872d757257be5898019c85e7971599065bc55065dc99d", size = 383803 }, - { url = "https://files.pythonhosted.org/packages/59/65/afc6e62bb506a319ea67b694551dab4a7e6fb7bf604e9bd9f3e11d575fec/yarl-1.22.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a49370e8f711daec68d09b821a34e1167792ee2d24d405cbc2387be4f158b520", size = 374220 }, - { url = "https://files.pythonhosted.org/packages/e7/3d/68bf18d50dc674b942daec86a9ba922d3113d8399b0e52b9897530442da2/yarl-1.22.0-cp312-cp312-win32.whl", hash = "sha256:70dfd4f241c04bd9239d53b17f11e6ab672b9f1420364af63e8531198e3f5fe8", size = 81589 }, - { url = "https://files.pythonhosted.org/packages/c8/9a/6ad1a9b37c2f72874f93e691b2e7ecb6137fb2b899983125db4204e47575/yarl-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:8884d8b332a5e9b88e23f60bb166890009429391864c685e17bd73a9eda9105c", size = 87213 }, - { url = "https://files.pythonhosted.org/packages/44/c5/c21b562d1680a77634d748e30c653c3ca918beb35555cff24986fff54598/yarl-1.22.0-cp312-cp312-win_arm64.whl", hash = "sha256:ea70f61a47f3cc93bdf8b2f368ed359ef02a01ca6393916bc8ff877427181e74", size = 81330 }, - { url = "https://files.pythonhosted.org/packages/ea/f3/d67de7260456ee105dc1d162d43a019ecad6b91e2f51809d6cddaa56690e/yarl-1.22.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8dee9c25c74997f6a750cd317b8ca63545169c098faee42c84aa5e506c819b53", size = 139980 }, - { url = "https://files.pythonhosted.org/packages/01/88/04d98af0b47e0ef42597b9b28863b9060bb515524da0a65d5f4db160b2d5/yarl-1.22.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:01e73b85a5434f89fc4fe27dcda2aff08ddf35e4d47bbbea3bdcd25321af538a", size = 93424 }, - { url = "https://files.pythonhosted.org/packages/18/91/3274b215fd8442a03975ce6bee5fe6aa57a8326b29b9d3d56234a1dca244/yarl-1.22.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:22965c2af250d20c873cdbee8ff958fb809940aeb2e74ba5f20aaf6b7ac8c70c", size = 93821 }, - { url = "https://files.pythonhosted.org/packages/61/3a/caf4e25036db0f2da4ca22a353dfeb3c9d3c95d2761ebe9b14df8fc16eb0/yarl-1.22.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4f15793aa49793ec8d1c708ab7f9eded1aa72edc5174cae703651555ed1b601", size = 373243 }, - { url = "https://files.pythonhosted.org/packages/6e/9e/51a77ac7516e8e7803b06e01f74e78649c24ee1021eca3d6a739cb6ea49c/yarl-1.22.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5542339dcf2747135c5c85f68680353d5cb9ffd741c0f2e8d832d054d41f35a", size = 342361 }, - { url = "https://files.pythonhosted.org/packages/d4/f8/33b92454789dde8407f156c00303e9a891f1f51a0330b0fad7c909f87692/yarl-1.22.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5c401e05ad47a75869c3ab3e35137f8468b846770587e70d71e11de797d113df", size = 387036 }, - { url = "https://files.pythonhosted.org/packages/d9/9a/c5db84ea024f76838220280f732970aa4ee154015d7f5c1bfb60a267af6f/yarl-1.22.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:243dda95d901c733f5b59214d28b0120893d91777cb8aa043e6ef059d3cddfe2", size = 397671 }, - { url = "https://files.pythonhosted.org/packages/11/c9/cd8538dc2e7727095e0c1d867bad1e40c98f37763e6d995c1939f5fdc7b1/yarl-1.22.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bec03d0d388060058f5d291a813f21c011041938a441c593374da6077fe21b1b", size = 377059 }, - { url = "https://files.pythonhosted.org/packages/a1/b9/ab437b261702ced75122ed78a876a6dec0a1b0f5e17a4ac7a9a2482d8abe/yarl-1.22.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0748275abb8c1e1e09301ee3cf90c8a99678a4e92e4373705f2a2570d581273", size = 365356 }, - { url = "https://files.pythonhosted.org/packages/b2/9d/8e1ae6d1d008a9567877b08f0ce4077a29974c04c062dabdb923ed98e6fe/yarl-1.22.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:47fdb18187e2a4e18fda2c25c05d8251a9e4a521edaed757fef033e7d8498d9a", size = 361331 }, - { url = "https://files.pythonhosted.org/packages/ca/5a/09b7be3905962f145b73beb468cdd53db8aa171cf18c80400a54c5b82846/yarl-1.22.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c7044802eec4524fde550afc28edda0dd5784c4c45f0be151a2d3ba017daca7d", size = 382590 }, - { url = "https://files.pythonhosted.org/packages/aa/7f/59ec509abf90eda5048b0bc3e2d7b5099dffdb3e6b127019895ab9d5ef44/yarl-1.22.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:139718f35149ff544caba20fce6e8a2f71f1e39b92c700d8438a0b1d2a631a02", size = 385316 }, - { url = "https://files.pythonhosted.org/packages/e5/84/891158426bc8036bfdfd862fabd0e0fa25df4176ec793e447f4b85cf1be4/yarl-1.22.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e1b51bebd221006d3d2f95fbe124b22b247136647ae5dcc8c7acafba66e5ee67", size = 374431 }, - { url = "https://files.pythonhosted.org/packages/bb/49/03da1580665baa8bef5e8ed34c6df2c2aca0a2f28bf397ed238cc1bbc6f2/yarl-1.22.0-cp313-cp313-win32.whl", hash = "sha256:d3e32536234a95f513bd374e93d717cf6b2231a791758de6c509e3653f234c95", size = 81555 }, - { url = "https://files.pythonhosted.org/packages/9a/ee/450914ae11b419eadd067c6183ae08381cfdfcb9798b90b2b713bbebddda/yarl-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:47743b82b76d89a1d20b83e60d5c20314cbd5ba2befc9cda8f28300c4a08ed4d", size = 86965 }, - { url = "https://files.pythonhosted.org/packages/98/4d/264a01eae03b6cf629ad69bae94e3b0e5344741e929073678e84bf7a3e3b/yarl-1.22.0-cp313-cp313-win_arm64.whl", hash = "sha256:5d0fcda9608875f7d052eff120c7a5da474a6796fe4d83e152e0e4d42f6d1a9b", size = 81205 }, - { url = "https://files.pythonhosted.org/packages/88/fc/6908f062a2f77b5f9f6d69cecb1747260831ff206adcbc5b510aff88df91/yarl-1.22.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:719ae08b6972befcba4310e49edb1161a88cdd331e3a694b84466bd938a6ab10", size = 146209 }, - { url = "https://files.pythonhosted.org/packages/65/47/76594ae8eab26210b4867be6f49129861ad33da1f1ebdf7051e98492bf62/yarl-1.22.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:47d8a5c446df1c4db9d21b49619ffdba90e77c89ec6e283f453856c74b50b9e3", size = 95966 }, - { url = "https://files.pythonhosted.org/packages/ab/ce/05e9828a49271ba6b5b038b15b3934e996980dd78abdfeb52a04cfb9467e/yarl-1.22.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cfebc0ac8333520d2d0423cbbe43ae43c8838862ddb898f5ca68565e395516e9", size = 97312 }, - { url = "https://files.pythonhosted.org/packages/d1/c5/7dffad5e4f2265b29c9d7ec869c369e4223166e4f9206fc2243ee9eea727/yarl-1.22.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4398557cbf484207df000309235979c79c4356518fd5c99158c7d38203c4da4f", size = 361967 }, - { url = "https://files.pythonhosted.org/packages/50/b2/375b933c93a54bff7fc041e1a6ad2c0f6f733ffb0c6e642ce56ee3b39970/yarl-1.22.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2ca6fd72a8cd803be290d42f2dec5cdcd5299eeb93c2d929bf060ad9efaf5de0", size = 323949 }, - { url = "https://files.pythonhosted.org/packages/66/50/bfc2a29a1d78644c5a7220ce2f304f38248dc94124a326794e677634b6cf/yarl-1.22.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca1f59c4e1ab6e72f0a23c13fca5430f889634166be85dbf1013683e49e3278e", size = 361818 }, - { url = "https://files.pythonhosted.org/packages/46/96/f3941a46af7d5d0f0498f86d71275696800ddcdd20426298e572b19b91ff/yarl-1.22.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c5010a52015e7c70f86eb967db0f37f3c8bd503a695a49f8d45700144667708", size = 372626 }, - { url = "https://files.pythonhosted.org/packages/c1/42/8b27c83bb875cd89448e42cd627e0fb971fa1675c9ec546393d18826cb50/yarl-1.22.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d7672ecf7557476642c88497c2f8d8542f8e36596e928e9bcba0e42e1e7d71f", size = 341129 }, - { url = "https://files.pythonhosted.org/packages/49/36/99ca3122201b382a3cf7cc937b95235b0ac944f7e9f2d5331d50821ed352/yarl-1.22.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b7c88eeef021579d600e50363e0b6ee4f7f6f728cd3486b9d0f3ee7b946398d", size = 346776 }, - { url = "https://files.pythonhosted.org/packages/85/b4/47328bf996acd01a4c16ef9dcd2f59c969f495073616586f78cd5f2efb99/yarl-1.22.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f4afb5c34f2c6fecdcc182dfcfc6af6cccf1aa923eed4d6a12e9d96904e1a0d8", size = 334879 }, - { url = "https://files.pythonhosted.org/packages/c2/ad/b77d7b3f14a4283bffb8e92c6026496f6de49751c2f97d4352242bba3990/yarl-1.22.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:59c189e3e99a59cf8d83cbb31d4db02d66cda5a1a4374e8a012b51255341abf5", size = 350996 }, - { url = "https://files.pythonhosted.org/packages/81/c8/06e1d69295792ba54d556f06686cbd6a7ce39c22307100e3fb4a2c0b0a1d/yarl-1.22.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5a3bf7f62a289fa90f1990422dc8dff5a458469ea71d1624585ec3a4c8d6960f", size = 356047 }, - { url = "https://files.pythonhosted.org/packages/4b/b8/4c0e9e9f597074b208d18cef227d83aac36184bfbc6eab204ea55783dbc5/yarl-1.22.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:de6b9a04c606978fdfe72666fa216ffcf2d1a9f6a381058d4378f8d7b1e5de62", size = 342947 }, - { url = "https://files.pythonhosted.org/packages/e0/e5/11f140a58bf4c6ad7aca69a892bff0ee638c31bea4206748fc0df4ebcb3a/yarl-1.22.0-cp313-cp313t-win32.whl", hash = "sha256:1834bb90991cc2999f10f97f5f01317f99b143284766d197e43cd5b45eb18d03", size = 86943 }, - { url = "https://files.pythonhosted.org/packages/31/74/8b74bae38ed7fe6793d0c15a0c8207bbb819cf287788459e5ed230996cdd/yarl-1.22.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff86011bd159a9d2dfc89c34cfd8aff12875980e3bd6a39ff097887520e60249", size = 93715 }, - { url = "https://files.pythonhosted.org/packages/69/66/991858aa4b5892d57aef7ee1ba6b4d01ec3b7eb3060795d34090a3ca3278/yarl-1.22.0-cp313-cp313t-win_arm64.whl", hash = "sha256:7861058d0582b847bc4e3a4a4c46828a410bca738673f35a29ba3ca5db0b473b", size = 83857 }, - { url = "https://files.pythonhosted.org/packages/46/b3/e20ef504049f1a1c54a814b4b9bed96d1ac0e0610c3b4da178f87209db05/yarl-1.22.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:34b36c2c57124530884d89d50ed2c1478697ad7473efd59cfd479945c95650e4", size = 140520 }, - { url = "https://files.pythonhosted.org/packages/e4/04/3532d990fdbab02e5ede063676b5c4260e7f3abea2151099c2aa745acc4c/yarl-1.22.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:0dd9a702591ca2e543631c2a017e4a547e38a5c0f29eece37d9097e04a7ac683", size = 93504 }, - { url = "https://files.pythonhosted.org/packages/11/63/ff458113c5c2dac9a9719ac68ee7c947cb621432bcf28c9972b1c0e83938/yarl-1.22.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:594fcab1032e2d2cc3321bb2e51271e7cd2b516c7d9aee780ece81b07ff8244b", size = 94282 }, - { url = "https://files.pythonhosted.org/packages/a7/bc/315a56aca762d44a6aaaf7ad253f04d996cb6b27bad34410f82d76ea8038/yarl-1.22.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3d7a87a78d46a2e3d5b72587ac14b4c16952dd0887dbb051451eceac774411e", size = 372080 }, - { url = "https://files.pythonhosted.org/packages/3f/3f/08e9b826ec2e099ea6e7c69a61272f4f6da62cb5b1b63590bb80ca2e4a40/yarl-1.22.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:852863707010316c973162e703bddabec35e8757e67fcb8ad58829de1ebc8590", size = 338696 }, - { url = "https://files.pythonhosted.org/packages/e3/9f/90360108e3b32bd76789088e99538febfea24a102380ae73827f62073543/yarl-1.22.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:131a085a53bfe839a477c0845acf21efc77457ba2bcf5899618136d64f3303a2", size = 387121 }, - { url = "https://files.pythonhosted.org/packages/98/92/ab8d4657bd5b46a38094cfaea498f18bb70ce6b63508fd7e909bd1f93066/yarl-1.22.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:078a8aefd263f4d4f923a9677b942b445a2be970ca24548a8102689a3a8ab8da", size = 394080 }, - { url = "https://files.pythonhosted.org/packages/f5/e7/d8c5a7752fef68205296201f8ec2bf718f5c805a7a7e9880576c67600658/yarl-1.22.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bca03b91c323036913993ff5c738d0842fc9c60c4648e5c8d98331526df89784", size = 372661 }, - { url = "https://files.pythonhosted.org/packages/b6/2e/f4d26183c8db0bb82d491b072f3127fb8c381a6206a3a56332714b79b751/yarl-1.22.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:68986a61557d37bb90d3051a45b91fa3d5c516d177dfc6dd6f2f436a07ff2b6b", size = 364645 }, - { url = "https://files.pythonhosted.org/packages/80/7c/428e5812e6b87cd00ee8e898328a62c95825bf37c7fa87f0b6bb2ad31304/yarl-1.22.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4792b262d585ff0dff6bcb787f8492e40698443ec982a3568c2096433660c694", size = 355361 }, - { url = "https://files.pythonhosted.org/packages/ec/2a/249405fd26776f8b13c067378ef4d7dd49c9098d1b6457cdd152a99e96a9/yarl-1.22.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ebd4549b108d732dba1d4ace67614b9545b21ece30937a63a65dd34efa19732d", size = 381451 }, - { url = "https://files.pythonhosted.org/packages/67/a8/fb6b1adbe98cf1e2dd9fad71003d3a63a1bc22459c6e15f5714eb9323b93/yarl-1.22.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f87ac53513d22240c7d59203f25cc3beac1e574c6cd681bbfd321987b69f95fd", size = 383814 }, - { url = "https://files.pythonhosted.org/packages/d9/f9/3aa2c0e480fb73e872ae2814c43bc1e734740bb0d54e8cb2a95925f98131/yarl-1.22.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:22b029f2881599e2f1b06f8f1db2ee63bd309e2293ba2d566e008ba12778b8da", size = 370799 }, - { url = "https://files.pythonhosted.org/packages/50/3c/af9dba3b8b5eeb302f36f16f92791f3ea62e3f47763406abf6d5a4a3333b/yarl-1.22.0-cp314-cp314-win32.whl", hash = "sha256:6a635ea45ba4ea8238463b4f7d0e721bad669f80878b7bfd1f89266e2ae63da2", size = 82990 }, - { url = "https://files.pythonhosted.org/packages/ac/30/ac3a0c5bdc1d6efd1b41fa24d4897a4329b3b1e98de9449679dd327af4f0/yarl-1.22.0-cp314-cp314-win_amd64.whl", hash = "sha256:0d6e6885777af0f110b0e5d7e5dda8b704efed3894da26220b7f3d887b839a79", size = 88292 }, - { url = "https://files.pythonhosted.org/packages/df/0a/227ab4ff5b998a1b7410abc7b46c9b7a26b0ca9e86c34ba4b8d8bc7c63d5/yarl-1.22.0-cp314-cp314-win_arm64.whl", hash = "sha256:8218f4e98d3c10d683584cb40f0424f4b9fd6e95610232dd75e13743b070ee33", size = 82888 }, - { url = "https://files.pythonhosted.org/packages/06/5e/a15eb13db90abd87dfbefb9760c0f3f257ac42a5cac7e75dbc23bed97a9f/yarl-1.22.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45c2842ff0e0d1b35a6bf1cd6c690939dacb617a70827f715232b2e0494d55d1", size = 146223 }, - { url = "https://files.pythonhosted.org/packages/18/82/9665c61910d4d84f41a5bf6837597c89e665fa88aa4941080704645932a9/yarl-1.22.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d947071e6ebcf2e2bee8fce76e10faca8f7a14808ca36a910263acaacef08eca", size = 95981 }, - { url = "https://files.pythonhosted.org/packages/5d/9a/2f65743589809af4d0a6d3aa749343c4b5f4c380cc24a8e94a3c6625a808/yarl-1.22.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:334b8721303e61b00019474cc103bdac3d7b1f65e91f0bfedeec2d56dfe74b53", size = 97303 }, - { url = "https://files.pythonhosted.org/packages/b0/ab/5b13d3e157505c43c3b43b5a776cbf7b24a02bc4cccc40314771197e3508/yarl-1.22.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e7ce67c34138a058fd092f67d07a72b8e31ff0c9236e751957465a24b28910c", size = 361820 }, - { url = "https://files.pythonhosted.org/packages/fb/76/242a5ef4677615cf95330cfc1b4610e78184400699bdda0acb897ef5e49a/yarl-1.22.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d77e1b2c6d04711478cb1c4ab90db07f1609ccf06a287d5607fcd90dc9863acf", size = 323203 }, - { url = "https://files.pythonhosted.org/packages/8c/96/475509110d3f0153b43d06164cf4195c64d16999e0c7e2d8a099adcd6907/yarl-1.22.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4647674b6150d2cae088fc07de2738a84b8bcedebef29802cf0b0a82ab6face", size = 363173 }, - { url = "https://files.pythonhosted.org/packages/c9/66/59db471aecfbd559a1fd48aedd954435558cd98c7d0da8b03cc6c140a32c/yarl-1.22.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efb07073be061c8f79d03d04139a80ba33cbd390ca8f0297aae9cce6411e4c6b", size = 373562 }, - { url = "https://files.pythonhosted.org/packages/03/1f/c5d94abc91557384719da10ff166b916107c1b45e4d0423a88457071dd88/yarl-1.22.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51ac5435758ba97ad69617e13233da53908beccc6cfcd6c34bbed8dcbede486", size = 339828 }, - { url = "https://files.pythonhosted.org/packages/5f/97/aa6a143d3afba17b6465733681c70cf175af89f76ec8d9286e08437a7454/yarl-1.22.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:33e32a0dd0c8205efa8e83d04fc9f19313772b78522d1bdc7d9aed706bfd6138", size = 347551 }, - { url = "https://files.pythonhosted.org/packages/43/3c/45a2b6d80195959239a7b2a8810506d4eea5487dce61c2a3393e7fc3c52e/yarl-1.22.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:bf4a21e58b9cde0e401e683ebd00f6ed30a06d14e93f7c8fd059f8b6e8f87b6a", size = 334512 }, - { url = "https://files.pythonhosted.org/packages/86/a0/c2ab48d74599c7c84cb104ebd799c5813de252bea0f360ffc29d270c2caa/yarl-1.22.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e4b582bab49ac33c8deb97e058cd67c2c50dac0dd134874106d9c774fd272529", size = 352400 }, - { url = "https://files.pythonhosted.org/packages/32/75/f8919b2eafc929567d3d8411f72bdb1a2109c01caaab4ebfa5f8ffadc15b/yarl-1.22.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0b5bcc1a9c4839e7e30b7b30dd47fe5e7e44fb7054ec29b5bb8d526aa1041093", size = 357140 }, - { url = "https://files.pythonhosted.org/packages/cf/72/6a85bba382f22cf78add705d8c3731748397d986e197e53ecc7835e76de7/yarl-1.22.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c0232bce2170103ec23c454e54a57008a9a72b5d1c3105dc2496750da8cfa47c", size = 341473 }, - { url = "https://files.pythonhosted.org/packages/35/18/55e6011f7c044dc80b98893060773cefcfdbf60dfefb8cb2f58b9bacbd83/yarl-1.22.0-cp314-cp314t-win32.whl", hash = "sha256:8009b3173bcd637be650922ac455946197d858b3630b6d8787aa9e5c4564533e", size = 89056 }, - { url = "https://files.pythonhosted.org/packages/f9/86/0f0dccb6e59a9e7f122c5afd43568b1d31b8ab7dda5f1b01fb5c7025c9a9/yarl-1.22.0-cp314-cp314t-win_amd64.whl", hash = "sha256:9fb17ea16e972c63d25d4a97f016d235c78dd2344820eb35bc034bc32012ee27", size = 96292 }, - { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171 }, - { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814 }, -] - -[[package]] -name = "zipp" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276 }, -] diff --git a/tests/test_agent_evaluation.py b/tests/test_agent_evaluation.py deleted file mode 100644 index 2ec52d0c0..000000000 --- a/tests/test_agent_evaluation.py +++ /dev/null @@ -1,512 +0,0 @@ -""" -Agent Evaluation Tests -====================== -Pytest-based evaluation tests for the single agent. - -These tests can be run: -1. Locally with MCP server running: `pytest tests/test_agent_evaluation.py -v` -2. In CI/CD pipeline against deployed services - -Markers: -- @pytest.mark.evaluation: All evaluation tests -- @pytest.mark.slow: Tests that take longer (full evaluation) -- @pytest.mark.unit: Fast unit tests for evaluation utilities -""" - -import asyncio -import json -import os -import sys -from pathlib import Path -from typing import Any, Dict, List -from unittest.mock import AsyncMock, MagicMock, patch - -import pytest - -# Add paths for imports - use absolute paths for reliability -_tests_dir = Path(__file__).parent.resolve() -_repo_root = _tests_dir.parent.resolve() -sys.path.insert(0, str(_tests_dir / "evaluation")) -sys.path.insert(0, str(_tests_dir)) -sys.path.insert(0, str(_repo_root / "agentic_ai" / "applications")) -sys.path.insert(0, str(_repo_root / "agentic_ai")) - -from evaluation.agent_evaluator import ( - AgentEvaluator, - AgentResponse, - AgentRunner, - EvaluationThresholds, - TestCase, - ToolCallTracker, - load_test_data, -) - - -# ============================================================================ -# Fixtures -# ============================================================================ - -@pytest.fixture -def sample_test_case() -> TestCase: - """Create a sample test case for testing.""" - return TestCase( - query="What's my billing summary?", - customer_id="251", - expected_intent="billing_inquiry", - expected_tools=["get_billing_summary", "get_customer_detail"], - ground_truth="The agent should retrieve and present the customer's billing summary.", - category="billing", - complexity="low", - ) - - -@pytest.fixture -def sample_agent_response(sample_test_case: TestCase) -> AgentResponse: - """Create a sample agent response for testing.""" - return AgentResponse( - test_case=sample_test_case, - response="Based on your account, your current billing summary shows an outstanding balance of $150.00. This includes your monthly subscription of $99.99 and additional data usage charges of $50.01.", - tools_called=["get_customer_detail", "get_billing_summary"], - execution_time_ms=1500.0, - error=None, - ) - - -@pytest.fixture -def test_data_path() -> str: - """Get the path to the test data file.""" - return str(Path(__file__).parent / "evaluation" / "test_data.jsonl") - - -@pytest.fixture -def evaluator() -> AgentEvaluator: - """Create an evaluator instance with default thresholds.""" - return AgentEvaluator(thresholds=EvaluationThresholds()) - - -# ============================================================================ -# Unit Tests - Evaluation Utilities -# ============================================================================ - -@pytest.mark.unit -class TestTestCase: - """Tests for TestCase dataclass.""" - - def test_from_dict(self): - """Test creating TestCase from dictionary.""" - data = { - "query": "Test query", - "customer_id": "123", - "expected_intent": "test_intent", - "expected_tools": ["tool1", "tool2"], - "ground_truth": "Expected response", - "category": "test", - "complexity": "low", - } - - test_case = TestCase.from_dict(data) - - assert test_case.query == "Test query" - assert test_case.customer_id == "123" - assert test_case.expected_intent == "test_intent" - assert test_case.expected_tools == ["tool1", "tool2"] - assert test_case.ground_truth == "Expected response" - assert test_case.category == "test" - assert test_case.complexity == "low" - - -@pytest.mark.unit -class TestToolCallTracker: - """Tests for ToolCallTracker.""" - - @pytest.mark.asyncio - async def test_tracks_tool_calls(self): - """Test that tool calls are tracked correctly.""" - tracker = ToolCallTracker() - - await tracker.broadcast("session1", {"type": "tool_called", "tool_name": "get_customer_detail"}) - await tracker.broadcast("session1", {"type": "tool_called", "tool_name": "get_billing_summary"}) - - tools = tracker.get_tools_called() - assert "get_customer_detail" in tools - assert "get_billing_summary" in tools - assert len(tools) == 2 - - @pytest.mark.asyncio - async def test_ignores_non_tool_events(self): - """Test that non-tool events are ignored.""" - tracker = ToolCallTracker() - - await tracker.broadcast("session1", {"type": "agent_start"}) - await tracker.broadcast("session1", {"type": "agent_token", "content": "Hello"}) - - tools = tracker.get_tools_called() - assert len(tools) == 0 - - @pytest.mark.asyncio - async def test_deduplicates_tool_calls(self): - """Test that duplicate tool calls are not counted twice.""" - tracker = ToolCallTracker() - - await tracker.broadcast("session1", {"type": "tool_called", "tool_name": "get_customer_detail"}) - await tracker.broadcast("session1", {"type": "tool_called", "tool_name": "get_customer_detail"}) - - tools = tracker.get_tools_called() - assert len(tools) == 1 - - -@pytest.mark.unit -class TestLoadTestData: - """Tests for test data loading.""" - - def test_load_test_data(self, test_data_path: str): - """Test loading test data from JSONL file.""" - test_cases = load_test_data(test_data_path) - - assert len(test_cases) > 0 - assert all(isinstance(tc, TestCase) for tc in test_cases) - - # Check first test case has expected fields - first_case = test_cases[0] - assert first_case.query - assert first_case.customer_id - assert first_case.expected_intent - assert len(first_case.expected_tools) > 0 - - def test_load_test_data_categories(self, test_data_path: str): - """Test that test data covers multiple categories.""" - test_cases = load_test_data(test_data_path) - - categories = set(tc.category for tc in test_cases) - - # Should have at least billing and support categories - assert "billing" in categories - assert len(categories) >= 3 # At least 3 different categories - - -# ============================================================================ -# Unit Tests - Evaluator -# ============================================================================ - -@pytest.mark.unit -class TestAgentEvaluator: - """Tests for AgentEvaluator.""" - - def test_evaluate_tool_accuracy_perfect_match(self, evaluator: AgentEvaluator, sample_agent_response: AgentResponse): - """Test tool accuracy with perfect match.""" - result = evaluator.evaluate_tool_accuracy(sample_agent_response) - - assert result["tool_precision"] == 1.0 - assert result["tool_recall"] == 1.0 - assert result["tool_f1_score"] == 1.0 - assert result["passed"] is True - assert len(result["missing_tools"]) == 0 - assert len(result["extra_tools"]) == 0 - - def test_evaluate_tool_accuracy_missing_tools(self, evaluator: AgentEvaluator, sample_test_case: TestCase): - """Test tool accuracy when expected tools are missing.""" - response = AgentResponse( - test_case=sample_test_case, - response="Some response", - tools_called=["get_customer_detail"], # Missing get_billing_summary - execution_time_ms=1000.0, - ) - - result = evaluator.evaluate_tool_accuracy(response) - - assert result["tool_recall"] == 0.5 - assert result["tool_precision"] == 1.0 - assert result["missing_tools"] == ["get_billing_summary"] - - def test_evaluate_tool_accuracy_extra_tools(self, evaluator: AgentEvaluator, sample_test_case: TestCase): - """Test tool accuracy when extra tools are called.""" - response = AgentResponse( - test_case=sample_test_case, - response="Some response", - tools_called=["get_customer_detail", "get_billing_summary", "get_promotions"], - execution_time_ms=1000.0, - ) - - result = evaluator.evaluate_tool_accuracy(response) - - assert result["tool_recall"] == 1.0 - assert result["tool_precision"] < 1.0 - assert "get_promotions" in result["extra_tools"] - - def test_evaluate_tool_accuracy_no_tools_called(self, evaluator: AgentEvaluator, sample_test_case: TestCase): - """Test tool accuracy when no tools are called.""" - response = AgentResponse( - test_case=sample_test_case, - response="I cannot help with that.", - tools_called=[], - execution_time_ms=500.0, - ) - - result = evaluator.evaluate_tool_accuracy(response) - - assert result["tool_recall"] == 0.0 - assert result["passed"] is False - - def test_evaluate_response_quality_good_response(self, evaluator: AgentEvaluator, sample_agent_response: AgentResponse): - """Test response quality with a good response.""" - result = evaluator.evaluate_response_quality(sample_agent_response) - - assert result["has_content"] is True - assert result["word_count"] > 10 - assert result["passed"] is True - - def test_evaluate_response_quality_empty_response(self, evaluator: AgentEvaluator, sample_test_case: TestCase): - """Test response quality with empty response.""" - response = AgentResponse( - test_case=sample_test_case, - response="", - tools_called=[], - execution_time_ms=500.0, - ) - - result = evaluator.evaluate_response_quality(response) - - assert result["has_content"] is False - assert result["passed"] is False - - def test_evaluate_response_quality_with_error(self, evaluator: AgentEvaluator, sample_test_case: TestCase): - """Test response quality when there's an error.""" - response = AgentResponse( - test_case=sample_test_case, - response="", - tools_called=[], - execution_time_ms=100.0, - error="Connection timeout", - ) - - result = evaluator.evaluate_response_quality(response) - - assert result["has_error"] is True - assert result["passed"] is False - - -# ============================================================================ -# Integration Tests - Full Evaluation Pipeline -# ============================================================================ - -@pytest.mark.evaluation -@pytest.mark.integration -class TestAgentEvaluationIntegration: - """ - Integration tests that run the actual agent against test cases. - - These tests require: - - MCP server running - - Azure OpenAI credentials configured - """ - - @pytest.fixture - def check_environment(self): - """Check that required environment variables are set.""" - required_vars = [ - "AZURE_OPENAI_ENDPOINT", - "AZURE_OPENAI_CHAT_DEPLOYMENT", - "MCP_SERVER_URI", - ] - - missing = [var for var in required_vars if not os.getenv(var)] - - if missing: - pytest.skip(f"Missing required environment variables: {', '.join(missing)}") - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_single_agent_billing_query(self, check_environment, test_data_path: str): - """Test single agent with a billing query.""" - test_cases = load_test_data(test_data_path) - - # Find a billing test case - billing_case = next((tc for tc in test_cases if tc.category == "billing"), None) - if not billing_case: - pytest.skip("No billing test case found") - - runner = AgentRunner(agent_module="agents.agent_framework.single_agent") - response = await runner.run_single_test(billing_case) - - # Basic assertions - assert response.response, "Agent should return a response" - assert response.error is None, f"Agent should not error: {response.error}" - - # Evaluate the response - evaluator = AgentEvaluator() - result = await evaluator.evaluate_response(response, include_ai_eval=False) - - print(f"\nTest Case: {billing_case.query}") - print(f"Response: {response.response[:200]}...") - print(f"Tools Called: {response.tools_called}") - print(f"Tool F1 Score: {result['tool_accuracy']['tool_f1_score']:.2f}") - print(f"Passed: {result['passed']}") - - @pytest.mark.asyncio - @pytest.mark.slow - async def test_full_evaluation_pipeline(self, check_environment, test_data_path: str): - """ - Run the full evaluation pipeline on all test cases. - - This is a comprehensive test that runs all test cases and generates - evaluation metrics. Use with caution as it can be slow and costly. - """ - test_cases = load_test_data(test_data_path) - - # Limit to first 3 cases for CI to save time/cost - test_cases = test_cases[:3] - - runner = AgentRunner(agent_module="agents.agent_framework.single_agent") - responses = await runner.run_test_dataset(test_cases) - - evaluator = AgentEvaluator() - results = await evaluator.evaluate_all(responses, include_ai_eval=False) - - summary = results["summary"] - - print(f"\n{'='*60}") - print("EVALUATION RESULTS") - print(f"{'='*60}") - print(f"Total: {summary['total_tests']}") - print(f"Passed: {summary['passed']}") - print(f"Pass Rate: {summary['pass_rate']:.1%}") - print(f"Avg Tool F1: {summary['average_tool_f1_score']:.2f}") - - # Assertions for CI/CD gates - # Note: Tool F1 may be lower because agent uses subset of expected tools - # The key is that agent provides helpful responses - assert summary["average_tool_f1_score"] >= 0.3, "Average tool F1 should be at least 0.3" - - # Print individual results for debugging - for i, result in enumerate(results["individual_results"]): - print(f"\nTest {i+1}: {result['test_case']['query'][:50]}...") - print(f" Tools Called: {result['tool_accuracy']['called_tools']}") - print(f" Tool F1: {result['tool_accuracy']['tool_f1_score']:.2f}") - print(f" Response OK: {result['response_quality']['has_content']}") - - -# ============================================================================ -# Mocked Tests - For CI/CD without live services -# ============================================================================ - -@pytest.mark.evaluation -@pytest.mark.unit -class TestAgentEvaluationMocked: - """ - Mocked evaluation tests that don't require live services. - These tests verify the evaluation logic works correctly. - """ - - @pytest.mark.asyncio - async def test_evaluation_with_mocked_agent(self, test_data_path: str): - """Test evaluation pipeline with mocked agent responses.""" - test_cases = load_test_data(test_data_path)[:2] - - # Create mock responses - mock_responses = [ - AgentResponse( - test_case=tc, - response=f"Here is the information for customer {tc.customer_id}: " + - "Your account shows normal activity. " * 10, - tools_called=tc.expected_tools[:2], # Simulate calling some expected tools - execution_time_ms=1500.0, - ) - for tc in test_cases - ] - - evaluator = AgentEvaluator() - results = await evaluator.evaluate_all(mock_responses, include_ai_eval=False) - - assert results["summary"]["total_tests"] == 2 - assert "individual_results" in results - assert len(results["individual_results"]) == 2 - - @pytest.mark.asyncio - async def test_evaluation_handles_errors_gracefully(self): - """Test that evaluation handles agent errors gracefully.""" - test_case = TestCase( - query="Test query", - customer_id="999", - expected_intent="test", - expected_tools=["some_tool"], - ground_truth="Expected response", - category="test", - complexity="low", - ) - - error_response = AgentResponse( - test_case=test_case, - response="", - tools_called=[], - execution_time_ms=100.0, - error="Agent initialization failed", - ) - - evaluator = AgentEvaluator() - result = await evaluator.evaluate_response(error_response, include_ai_eval=False) - - assert result["passed"] is False - assert result["error"] == "Agent initialization failed" - assert result["response_quality"]["has_error"] is True - - -# ============================================================================ -# Threshold Tests -# ============================================================================ - -@pytest.mark.evaluation -@pytest.mark.unit -class TestEvaluationThresholds: - """Tests for evaluation threshold configuration.""" - - def test_default_thresholds(self): - """Test that default thresholds are reasonable.""" - thresholds = EvaluationThresholds() - - assert thresholds.tool_call_accuracy == 0.5 # Lower threshold for single agent - assert thresholds.groundedness == 0.7 - assert thresholds.relevance == 0.8 - - def test_custom_thresholds(self): - """Test that custom thresholds can be set.""" - thresholds = EvaluationThresholds( - tool_call_accuracy=0.9, - groundedness=0.9, - ) - - assert thresholds.tool_call_accuracy == 0.9 - assert thresholds.groundedness == 0.9 - - def test_strict_thresholds_fail_more(self, sample_test_case: TestCase): - """Test that stricter thresholds cause more failures.""" - response = AgentResponse( - test_case=sample_test_case, - response="Brief response.", - tools_called=["get_customer_detail"], # Only 1 of 2 expected tools - execution_time_ms=1000.0, - ) - - # Default threshold (0.5) - should pass with F1 ~0.67 - default_evaluator = AgentEvaluator(thresholds=EvaluationThresholds()) - default_result = default_evaluator.evaluate_tool_accuracy(response) - - # Strict threshold (0.9) - should fail - strict_evaluator = AgentEvaluator(thresholds=EvaluationThresholds(tool_call_accuracy=0.9)) - strict_result = strict_evaluator.evaluate_tool_accuracy(response) - - # F1 score of ~0.67 passes 0.5 threshold but fails 0.9 - assert default_result["passed"] is True # 0.67 >= 0.5 - assert strict_result["passed"] is False # 0.67 < 0.9 - - -# ============================================================================ -# CLI Runner Test -# ============================================================================ - -@pytest.mark.evaluation -@pytest.mark.unit -def test_cli_can_import(): - """Test that the evaluation module can be imported for CLI use.""" - from evaluation.agent_evaluator import run_evaluation - - assert callable(run_evaluation)