diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..3b69a6f36c244e201e52b84afd87fa53df5c7fc5 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +TAVILY_API_KEY= +OPENAI_API_KEY= +XAI_API_KEY= +ANTHROPIC_API_KEY= +GOOGLE_API_KEY= +AI_MODEL="gpt-5/grok-4-1/claude-4-5" \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000000000000000000000000000000..6b9b464764373292a803cf3d3c22a42a00f4fab0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: rajatsandeepsen diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000000000000000000000000000000000000..54199a8d454149547a3587d329969c70063b7fdf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,37 @@ +name: 🐞 Bug Report +description: Create a bug report to help us improve +title: "bug: " +labels: ["🐞❔ unconfirmed bug"] +body: + - type: textarea + attributes: + label: Provide environment information + description: | + Run this command in your project root and paste the results in a code block: + ```bash + npx envinfo --system --binaries + ``` + validations: + required: true + - type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of the bug, as well as what you expected to happen when encountering it. + validations: + required: true + - type: input + attributes: + label: Link to reproduction + description: Please provide a link to a reproduction of the bug. Issues without a reproduction repo may be ignored. + validations: + required: true + - type: textarea + attributes: + label: To reproduce + description: Describe how to reproduce your bug. Steps, code snippets, reproduction repos etc. + validations: + required: true + - type: textarea + attributes: + label: Additional information + description: Add any other information related to the bug here, screenshots if applicable. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000000000000000000000000000000000000..ae1ccf2dad896a105d9e6da49d8e238b0d739463 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,29 @@ +# This template is heavily inspired by the Next.js's template: +# See here: https://github.com/vercel/next.js/blob/canary/.github/ISSUE_TEMPLATE/3.feature_request.yml + +name: 🛠 Feature Request +description: Create a feature request for the core packages +title: "feat: " +labels: ["✨ enhancement"] +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to file a feature request. Please fill out this form as completely as possible. + - type: textarea + attributes: + label: Describe the feature you'd like to request + description: Please describe the feature as clear and concise as possible. Remember to add context as to why you believe this feature is needed. + validations: + required: true + - type: textarea + attributes: + label: Describe the solution you'd like to see + description: Please describe the solution you would like to see. Adding example usage is a good way to provide context. + validations: + required: true + - type: textarea + attributes: + label: Additional information + description: Add any other information related to the feature here. If your feature request is related to any issues or discussions, link them here. + diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000000000000000000000000000000000000..9784cb3ab74ebd0b72b5af3b6867ab2219d58d11 --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:base"], + "packageRules": [ + { + "matchPackagePatterns": ["^@decode/"], + "enabled": false + } + ], + "updateInternalDeps": true, + "rangeStrategy": "bump", + "automerge": true +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..03fe1468e314fabcfa4dca5ec3c21a4a40c82dde --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: + pull_request: + branches: ["*"] + push: + branches: ["main"] + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} + +# You can leverage Vercel Remote Caching with Turbo to speed up your builds +# @link https://turborepo.org/docs/core-concepts/remote-caching#remote-caching-on-vercel-builds +env: + FORCE_COLOR: 3 + TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} + TURBO_TEAM: ${{ vars.TURBO_TEAM }} + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup + uses: ./tooling/github/setup + + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + + - name: Copy env + shell: bash + run: cp .env.example .env + + - name: Lint + run: pnpm lint + + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup + uses: ./tooling/github/setup + + - name: Setup Biome + uses: biomejs/setup-biome@v2 + with: + version: latest + + - name: Format + run: pnpm format + + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup + uses: ./tooling/github/setup + + - name: Typecheck + run: turbo typecheck diff --git a/.github/workflows/deploy-hf-space.yml b/.github/workflows/deploy-hf-space.yml new file mode 100644 index 0000000000000000000000000000000000000000..f897d5636cd7eee5df7044ea769d9de081603a86 --- /dev/null +++ b/.github/workflows/deploy-hf-space.yml @@ -0,0 +1,38 @@ +name: Deploy to Hugging Face Spaces + +on: + push: + branches: [main] + workflow_dispatch: + +env: + HF_SPACE: MCP-1st-Birthday/eu-ai-act-compliance-agent + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Push to Hugging Face Space + env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} + run: | + # Configure git + git config --global user.email "github-actions@github.com" + git config --global user.name "GitHub Actions" + + # Copy Space files from apps/eu-ai-act-agent to root + cp apps/eu-ai-act-agent/README_HF.md ./README.md + cp apps/eu-ai-act-agent/Dockerfile ./Dockerfile + + # Add HF Space remote and push + git remote add hf https://user:${HF_TOKEN}@huggingface.co/spaces/${HF_SPACE} || true + git add -A + git commit -m "Deploy to HF Spaces" --allow-empty + git push hf main:main --force + + echo "✅ Deployed to https://huggingface.co/spaces/${HF_SPACE}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..45f18c170dec80263ea81f5c9a6946c97e42f3d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# next.js +.next/ +out/ +next-env.d.ts + +# nitro +.nitro/ +.output/ + +# expo +.expo/ +dist/ +expo-env.d.ts +apps/expo/.gitignore + +# production +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo + +# turbo +.turbo +/.history + +# generated compliance documentation +compliance-docs/ +/modal/__pycache__ +modal/__pycache__/gpt_oss_inference.cpython-313.pyc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000000000000000000000000000000000..cfcb8e31b11fbb3ed663dd3f8e62a50bf6da2508 --- /dev/null +++ b/.npmrc @@ -0,0 +1,5 @@ +# Expo doesn't play nice with pnpm by default. +# The symbolic links of pnpm break the rules of Expo monorepos. +# @link https://docs.expo.dev/guides/monorepos/#common-issues +node-linker=hoisted +strict-peer-dependencies=false diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000000000000000000000000000000000..87ec8842b158d213e0477ba0129281a484b9d47d --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.18.2 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000000000000000000000000000000000..511e4899865649b1a85bc522ae8d11dae030dc23 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "bradlc.vscode-tailwindcss", + "expo.vscode-expo-tools", + "yoavbls.pretty-ts-errors", + "biomejs.biome" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..8b2486d8bc474b9b26632f9450e403d402565c11 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js", + "type": "node-terminal", + "request": "launch", + "command": "pnpm dev", + "cwd": "${workspaceFolder}/apps/web/", + "skipFiles": [ + "/**" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/project.code-workspace b/.vscode/project.code-workspace new file mode 100644 index 0000000000000000000000000000000000000000..9f08745bc1adca53e81f0583c26aa3cf1af49a3c --- /dev/null +++ b/.vscode/project.code-workspace @@ -0,0 +1,36 @@ +{ + "folders": [ + { + "path": "..", + "name": "root" + }, + { + "name": "native", + "path": "../apps/native/" + }, + { + "name": "web", + "path": "../apps/web/" + }, + { + "name": "auth-proxy", + "path": "../apps/auth-proxy/" + }, + { + "name": "api", + "path": "../packages/api/" + }, + { + "name": "db", + "path": "../packages/db/" + }, + { + "name": "auth", + "path": "../packages/auth/" + }, + { + "name": "tooling", + "path": "../tooling/" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..44a73ec3a98ec7820a3e9e77aebbad0625626393 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "eslint.workingDirectories": [ + { + "mode": "auto" + } + ] +} diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..1b51c157eefca378ceacdb313056eaf862267613 --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,335 @@ +# 🚀 Deployment Guide + +This guide covers deploying the EU AI Act Compliance Suite for the MCP 1st Birthday Hackathon. + +## 📋 Table of Contents + +- [Deployment Options](#deployment-options) +- [Hugging Face Spaces (Recommended)](#hugging-face-spaces-recommended) +- [Manual Deployment](#manual-deployment) +- [GitHub Actions CI/CD](#github-actions-cicd) +- [Environment Variables](#environment-variables) +- [Hackathon Submission Checklist](#hackathon-submission-checklist) + +--- + +## 🎯 Deployment Options + +| Option | Best For | Difficulty | +|--------|----------|------------| +| **Hugging Face Spaces** | Hackathon submission, public demos | ⭐ Easy | +| **Docker** | Self-hosted, production | ⭐⭐ Medium | +| **Local Development** | Testing, development | ⭐ Easy | + +--- + +## 🤗 Hugging Face Spaces (Recommended) + +The easiest way to deploy for the hackathon is using **Hugging Face Spaces**. + +### Method 1: Automated Deployment (GitHub Actions) + +1. **Fork this repository** to your GitHub account + +2. **Add GitHub Secrets:** + - Go to your repo → Settings → Secrets and variables → Actions + - Add `HF_TOKEN`: Your Hugging Face token with write access + + ```bash + # Get your HF token from: https://huggingface.co/settings/tokens + # Required scopes: write access to spaces + ``` + +3. **Join the Hackathon Organization:** + - Go to [MCP-1st-Birthday](https://huggingface.co/MCP-1st-Birthday) + - Click "Request to join this org" + - Wait for approval + +4. **Trigger Deployment:** + - Push to `main` branch (auto-deploys on changes to `spaces/` directory) + - Or manually trigger via GitHub Actions → "Deploy to Hugging Face Spaces" → "Run workflow" + +5. **Configure Space Secrets:** + - Go to your Space settings: `https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-compliance/settings` + - Add secrets: + - `XAI_API_KEY` (required) - Get from [x.ai](https://x.ai/) + - `TAVILY_API_KEY` (optional) - Get from [tavily.com](https://app.tavily.com/) + +### Method 2: Manual Upload + +1. **Create a new Space:** + ```bash + # Install huggingface_hub + pip install huggingface_hub + + # Login + huggingface-cli login + + # Create space + huggingface-cli repo create eu-ai-act-compliance --type space --space-sdk gradio + ``` + +2. **Upload files:** + ```bash + cd spaces/eu-ai-act-compliance + + # Clone the space + git clone https://huggingface.co/spaces/YOUR_USERNAME/eu-ai-act-compliance + + # Copy files + cp -r . eu-ai-act-compliance/ + + # Push + cd eu-ai-act-compliance + git add . + git commit -m "Initial deployment" + git push + ``` + +3. **Transfer to hackathon org** (for submission): + - Go to Space Settings → Transfer + - Transfer to `MCP-1st-Birthday` organization + +### Method 3: Using the Deploy Script + +```bash +# Run the deployment script +./scripts/deploy-hf.sh + +# With custom org/name +./scripts/deploy-hf.sh --org MCP-1st-Birthday --name eu-ai-act-compliance +``` + +--- + +## 🐳 Docker Deployment + +### Build and Run + +```bash +# Build the image +docker build -t eu-ai-act-compliance -f Dockerfile . + +# Run with environment variables +docker run -p 7860:7860 \ + -e XAI_API_KEY=your-key \ + -e TAVILY_API_KEY=your-key \ + eu-ai-act-compliance +``` + +### Docker Compose + +```yaml +version: '3.8' +services: + eu-ai-act-agent: + build: . + ports: + - "7860:7860" + environment: + - XAI_API_KEY=${XAI_API_KEY} + - TAVILY_API_KEY=${TAVILY_API_KEY} + restart: unless-stopped +``` + +--- + +## 🔧 Manual Deployment + +### Prerequisites + +- Node.js 18+ +- Python 3.9+ +- pnpm 8+ + +### Steps + +```bash +# 1. Clone the repository +git clone https://github.com/your-org/eu-ai-act-compliance.git +cd eu-ai-act-compliance + +# 2. Install dependencies +pnpm install + +# 3. Set up environment variables +cp .env.example .env +# Edit .env and add your API keys + +# 4. Build the MCP server +pnpm --filter @eu-ai-act/mcp-server build + +# 5. Start the agent (API + Gradio) +cd apps/eu-ai-act-agent +./start.sh +``` + +### Production Mode + +```bash +# Build everything +pnpm build + +# Start in production +cd apps/eu-ai-act-agent +NODE_ENV=production node dist/server.js & +python src/gradio_app.py +``` + +--- + +## 🔄 GitHub Actions CI/CD + +### Workflows + +| Workflow | Trigger | Purpose | +|----------|---------|---------| +| `ci.yml` | Push/PR | Lint, typecheck, build | +| `deploy-hf-space.yml` | Push to main + `spaces/` changes | Deploy to HF Spaces | + +### Required Secrets + +| Secret | Required | Description | +|--------|----------|-------------| +| `HF_TOKEN` | Yes | Hugging Face token with write access | + +### Manual Deployment Trigger + +1. Go to Actions → "Deploy to Hugging Face Spaces" +2. Click "Run workflow" +3. Select branch and environment +4. Click "Run workflow" + +--- + +## 🔐 Environment Variables + +### Required + +| Variable | Description | Where to Get | +|----------|-------------|--------------| +| `XAI_API_KEY` | xAI API key for Grok model | [console.x.ai](https://console.x.ai/) | + +### Optional + +| Variable | Description | Where to Get | +|----------|-------------|--------------| +| `TAVILY_API_KEY` | Tavily API for web research | [app.tavily.com](https://app.tavily.com/) | +| `PORT` | API server port (default: 3001) | - | + +### Setting Secrets in Hugging Face Spaces + +1. Go to your Space: `https://huggingface.co/spaces/ORG/SPACE_NAME` +2. Click ⚙️ Settings +3. Scroll to "Repository secrets" +4. Add each secret: + - Name: `XAI_API_KEY` + - Value: Your API key + - Click "Add" + +--- + +## ✅ Hackathon Submission Checklist + +### Before Submission (Nov 30, 2025 11:59 PM UTC) + +- [ ] **Join the organization**: [Request to join MCP-1st-Birthday](https://huggingface.co/MCP-1st-Birthday) +- [ ] **Deploy your Space**: Make sure it's running and accessible +- [ ] **Configure secrets**: Add `XAI_API_KEY` (and optionally `TAVILY_API_KEY`) +- [ ] **Test the demo**: Verify all features work + +### README Requirements + +Your Space README must include: + +- [ ] **Hackathon tags** in frontmatter: + ```yaml + tags: + - mcp + - agents + - track-1-mcp-servers + - track-2-agentic-applications + ``` + +- [ ] **Social media link**: Share your project and include the link + ```markdown + [🐦 Twitter Post](https://twitter.com/your-post-link) + ``` + +### Track Tags + +| Track | Tags | +|-------|------| +| Track 1: Building MCP | `track-1-mcp-servers`, `mcp` | +| Track 2: MCP in Action | `track-2-agentic-applications`, `agents` | + +### Social Media Post Template + +``` +🇪🇺 Excited to share my #MCPHackathon submission! + +EU AI Act Compliance Agent - AI-powered compliance assessment with MCP tools + +✅ Discover organization profiles +✅ Classify AI systems by risk +✅ Generate compliance documentation + +Try it: [HF Space Link] + +#MCP #AIAct #Gradio @huggingface +``` + +--- + +## 🔍 Troubleshooting + +### Space Not Building + +1. Check `requirements.txt` for valid packages +2. Verify Python version compatibility +3. Check build logs in Space settings + +### API Key Errors + +1. Verify secrets are set in Space settings +2. Check secret names match exactly (case-sensitive) +3. Ensure API keys are valid and have required permissions + +### Deployment Failing + +1. Check GitHub Actions logs +2. Verify `HF_TOKEN` has write access +3. Ensure you're a member of the target organization + +### Space Sleeping + +Free HF Spaces sleep after inactivity. To wake: +1. Visit the Space URL +2. Wait for it to build/start +3. Consider upgrading for persistent uptime + +--- + +## 📞 Support + +- **Hackathon Discord**: [#agents-mcp-hackathon-winter25🏆](https://discord.gg/huggingface) +- **GitHub Issues**: [Create an issue](https://github.com/your-org/eu-ai-act-compliance/issues) +- **Email**: gradio-team@huggingface.co + +--- + +## 📚 Additional Resources + +- [Hugging Face Spaces Documentation](https://huggingface.co/docs/hub/spaces) +- [Gradio Deployment Guide](https://www.gradio.app/guides/sharing-your-app) +- [MCP Course](https://huggingface.co/learn/mcp-course) +- [Hackathon Page](https://huggingface.co/MCP-1st-Birthday) + +--- + +
+ +**Good luck with your submission! 🎂** + +
+ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..73d2d0b5f656e4f2dd18686ea82ba4697766d3bf --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# EU AI Act - ChatGPT MCP Server +# Standalone MCP server for ChatGPT Apps integration +# Deploys ONLY the MCP tools (discover_organization, discover_ai_services, assess_compliance) + +FROM node:20-slim + +# Install Python, pnpm, and uv +RUN apt-get update && apt-get install -y \ + python3 \ + python3-venv \ + curl \ + && npm install -g pnpm \ + && curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh \ + && rm -rf /var/lib/apt/lists/* + +# Use existing node user (UID 1000) for HF Spaces compatibility +USER node +ENV HOME=/home/node + +WORKDIR $HOME/app + +# Copy entire monorepo +COPY --chown=node . . + +# Install Node dependencies +RUN pnpm install --frozen-lockfile + +# Build MCP server and Agent (needed for API) +RUN pnpm --filter @eu-ai-act/mcp-server build +RUN pnpm --filter @eu-ai-act/agent build + +# Create Python venv and install dependencies +RUN uv venv $HOME/venv && \ + . $HOME/venv/bin/activate && \ + uv pip install --no-cache -r apps/eu-ai-act-agent/requirements.txt + +# Environment +ENV NODE_ENV=production \ + PORT=3001 \ + API_URL=http://localhost:3001 \ + PUBLIC_URL=https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space \ + CHATGPT_APP_SERVER_NAME=0.0.0.0 \ + CHATGPT_APP_SERVER_PORT=7860 \ + PATH=/home/node/venv/bin:$PATH \ + VIRTUAL_ENV=/home/node/venv \ + MCP_SERVER_PATH=/home/node/app/packages/eu-ai-act-mcp/dist/index.js + +WORKDIR $HOME/app/apps/eu-ai-act-agent + +EXPOSE 7860 + +# Start API server + ChatGPT MCP App on port 7860 +# MCP URL will be: PUBLIC_URL/gradio_api/mcp/ +CMD node dist/server.js & \ + sleep 2 && \ + python src/chatgpt_app.py + diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..702854351f04dea15c6c0b640fd6019e91ddef00 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +--- +title: EU AI Act - ChatGPT MCP Server by legitima.ai +emoji: ⚖️ +colorFrom: blue +colorTo: indigo +sdk: docker +pinned: false +tags: + - building-mcp-track-enterprise + - mcp-in-action-track-enterprise +short_description: MCP Server for ChatGPT Apps - EU AI Act Compliance Tools +--- + +# 🇪🇺 EU AI Act - ChatGPT MCP Server by [legitima.ai](https://legitima.ai/mcp-hackathon) powered by [decode](https://decode.gr/en) + +
+ Gradio MCP Hackathon - EU AI Act Compliance +
+ +This is the **MCP Server** for integrating EU AI Act compliance tools with **ChatGPT Desktop**. + +## 🔗 MCP URL + +``` +https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/gradio_api/mcp/ +``` + +## 📖 How to Use in ChatGPT + +1. **Enable Developer Mode** in ChatGPT: Settings → Apps & Connectors → Advanced settings +2. **Create a Connector** with the MCP URL above (choose "No authentication") +3. **Chat with ChatGPT** using `@eu-ai-act` to access the tools + +## 🔧 Available MCP Tools + +| Tool | Description | +|------|-------------| +| `discover_organization` | Research and profile an organization for compliance | +| `discover_ai_services` | Discover and classify AI systems by risk level | +| `assess_compliance` | Generate compliance assessment and documentation | + +## 🤖 Main Agent UI + +For the full interactive chat experience, visit: +**[EU AI Act Compliance Agent](https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-compliance-agent)** + +--- + +Built for the **MCP 1st Birthday Hackathon** 🎂 + +**🔗 Demo & Showcase:** [www.legitima.ai/mcp-hackathon](https://www.legitima.ai/mcp-hackathon) +**📹 Video:** [Guiddes](https://app.guidde.com/share/playlists/2wXbDrSm2YY7YnWMJbftuu?origin=wywDANMIvNhPu9kYVOXCPpdFcya2) +**📱 Social Media:** [LinkedIn Post 1](https://www.linkedin.com/posts/iordanis-sarafidis_mcp-1st-birthday-mcp-1st-birthday-activity-7400132272282144768-ZIir?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs) + +[LinkedIn Post 2](https://www.linkedin.com/posts/billdrosatos_mcp-1st-birthday-mcp-1st-birthday-activity-7400135422502252544-C5BS?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs) diff --git a/RUN_LOCAL.sh b/RUN_LOCAL.sh new file mode 100755 index 0000000000000000000000000000000000000000..53f0e10d605e66b30ab1d8a4fc2f8b8bc8853a0f --- /dev/null +++ b/RUN_LOCAL.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# EU AI Act MCP Server - Local Testing Script +# This script builds and tests the MCP server + +set -e + +echo "🚀 EU AI Act MCP Server - Local Testing" +echo "========================================" +echo "" + +# Colors for output +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Check if we're in the right directory +if [ ! -f "package.json" ]; then + echo "❌ Error: Please run this script from the project root directory" + exit 1 +fi + +# Step 1: Install dependencies +echo -e "${BLUE}Step 1: Installing dependencies...${NC}" +pnpm install --filter @eu-ai-act/mcp-server --filter @eu-ai-act/test-agent +echo -e "${GREEN}✅ Dependencies installed${NC}" +echo "" + +# Step 2: Build MCP server +echo -e "${BLUE}Step 2: Building MCP server...${NC}" +pnpm --filter @eu-ai-act/mcp-server build +echo -e "${GREEN}✅ MCP server built successfully${NC}" +echo "" + +# Step 3: Run tests +echo -e "${BLUE}Step 3: Running test agent...${NC}" +echo "" +pnpm --filter @eu-ai-act/test-agent dev +echo "" + +# Success message +echo "" +echo -e "${GREEN}========================================" +echo "✅ All tests completed successfully!" +echo "========================================${NC}" +echo "" +echo -e "${YELLOW}Next Steps:${NC}" +echo "1. Configure Claude Desktop (see QUICKSTART.md)" +echo "2. Read packages/eu-ai-act-mcp/README.md for API docs" +echo "3. See IMPLEMENTATION.md for architecture details" +echo "" +echo "Your MCP server is ready to use! 🎉" + diff --git a/SUBMISSION.md b/SUBMISSION.md new file mode 100644 index 0000000000000000000000000000000000000000..1b3958032602ad2875afc9cefad23893227f4dc6 --- /dev/null +++ b/SUBMISSION.md @@ -0,0 +1,120 @@ +# 🎂 MCP 1st Birthday Hackathon Submission + +## EU AI Act Compliance Agent + +### 🔗 Links + +| Resource | URL | +| --------------- | ------------------------------------------------------------------------------- | +| **Live Demo** | [HF Space](https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-compliance) | +| **GitHub** | [Repository](https://github.com/your-org/eu-ai-act-compliance) | +| **Social Post** | [Twitter/X Post](#) | + +--- + +## 📋 Submission Details + +### Tracks + +- ✅ **Track 1: Building MCP** - MCP Server with compliance tools +- ✅ **Track 2: MCP in Action** - Agentic application with Gradio UI + +### Tags + +``` +mcp, agents, eu-ai-act, compliance, legal-tech, gradio, +track-1-mcp-servers, track-2-agentic-applications +``` + +--- + +## 🎯 Project Overview + +The **EU AI Act Compliance Agent** helps organizations navigate the European Union's AI Act (Regulation 2024/1689) — the world's first comprehensive AI regulation framework. + +### The Problem + +- 📋 **Complex Classification** — AI systems must be classified by risk level +- 📝 **Documentation** — Extensive technical documentation required +- 🔍 **Transparency** — Clear disclosure obligations +- ⏰ **Tight Deadlines** — Phased implementation starting 2025 + +### Our Solution + +Three MCP tools + AI Agent for automated compliance: + +| Tool | Purpose | +| ----------------------- | ------------------------------- | +| `discover_organization` | Research & profile organization | +| `discover_ai_services` | Find & classify AI systems | +| `assess_compliance` | Generate compliance assessment | + +--- + +## 🛠️ Tech Stack + +- **Gradio 6** — Interactive web UI +- **xAI Grok** — AI reasoning and tool calling +- **Tavily AI** — Web research +- **Model Context Protocol** — Tool integration + +--- + +## ✨ Key Features + +### Track 1: MCP Server + +1. **Organization Discovery** — Real-time web research using Tavily +2. **AI System Classification** — Risk tiers per EU AI Act Annex III +3. **Compliance Assessment** — Gap analysis with documentation templates + +### Track 2: AI Agent + +1. **Conversational Interface** — Natural language interaction +2. **Tool Orchestration** — Intelligent multi-tool workflows +3. **Document Generation** — Ready-to-use compliance templates +4. **Real-time Streaming** — Progressive response display + +--- + +## 📊 Demo Workflow + +``` +User: "Analyze OpenAI's EU AI Act compliance" + +Agent: +├── 🔧 discover_organization("OpenAI") +│ └── ✅ Found: AI company, Expert maturity, Provider role +├── 🔧 discover_ai_services(orgContext) +│ └── ✅ Found: 5 AI systems (2 high-risk, 3 limited-risk) +├── 🔧 assess_compliance(orgContext, servicesContext) +│ └── ✅ Score: 65/100, 3 critical gaps identified +└── 📝 Generated compliance report with recommendations +``` + +--- + +## 🏆 Why This Matters + +1. **Real Regulation** — EU AI Act is live, affecting millions of organizations +2. **Practical Tools** — Automates tedious compliance workflows +3. **Educational** — Helps understand complex legal requirements +4. **Actionable** — Generates usable documentation templates + +--- + +## 👥 Team + +**Team EU Compliance** + +Building the future of AI governance 🇪🇺 + +**** +
+ +**Built with ❤️ for the MCP 1st Birthday Hackathon** + +🎂 Happy 1st Birthday, MCP! 🎂 + +
+ diff --git a/apps/eu-ai-act-agent/.gitignore b/apps/eu-ai-act-agent/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..421472e66cce602608d712c95a56e48b78b4f690 --- /dev/null +++ b/apps/eu-ai-act-agent/.gitignore @@ -0,0 +1,52 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Build outputs +dist/ +build/ +*.tsbuildinfo + +# Environment +.env +.env.local +.env.*.local + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Testing +coverage/ +.nyc_output/ + +# Misc +.cache/ +temp/ +tmp/ + diff --git a/apps/eu-ai-act-agent/.python-version b/apps/eu-ai-act-agent/.python-version new file mode 100644 index 0000000000000000000000000000000000000000..8c47d44a36235d132af890d2a0960aed574c20e8 --- /dev/null +++ b/apps/eu-ai-act-agent/.python-version @@ -0,0 +1,2 @@ +3.10 + diff --git a/apps/eu-ai-act-agent/API.md b/apps/eu-ai-act-agent/API.md new file mode 100644 index 0000000000000000000000000000000000000000..70233fc63945b7bb0b708c3fddc53b06b287f141 --- /dev/null +++ b/apps/eu-ai-act-agent/API.md @@ -0,0 +1,579 @@ +# 🔌 API Reference + +Complete reference for the EU AI Act Compliance Agent API. + +## Base URL + +``` +http://localhost:3001 +``` + +## Authentication + +Currently no authentication required for local development. Add API key authentication for production deployment. + +--- + +## Endpoints + +### 1. Health Check + +Check if the API server is running and healthy. + +**Endpoint**: `GET /health` + +**Response**: +```json +{ + "status": "ok", + "service": "EU AI Act Compliance Agent", + "version": "0.1.0" +} +``` + +**Example**: +```bash +curl http://localhost:3001/health +``` + +--- + +### 2. Chat Endpoint + +Send a message to the AI agent and receive a streaming response. + +**Endpoint**: `POST /api/chat` + +**Content-Type**: `application/json` + +**Request Body**: +```json +{ + "message": "What is the EU AI Act?", + "history": [ + { + "role": "user", + "content": "Previous user message" + }, + { + "role": "assistant", + "content": "Previous assistant response" + } + ] +} +``` + +**Parameters**: +- `message` (string, required): The user's input message +- `history` (array, optional): Conversation history for context + +**Response Format**: Server-Sent Events (SSE) / Event Stream + +**Response Events**: + +1. **Text Chunk**: +```json +{ + "type": "text", + "content": "The EU AI Act is..." +} +``` + +2. **Tool Call** (when agent uses a tool): +```json +{ + "type": "tool_call", + "tool": "discover_organization", + "args": {...} +} +``` + +3. **Tool Result**: +```json +{ + "type": "tool_result", + "tool": "discover_organization", + "result": {...} +} +``` + +4. **Done**: +```json +{ + "type": "done" +} +``` + +5. **Error**: +```json +{ + "type": "error", + "error": "Error message" +} +``` + +**Example**: +```bash +curl -X POST http://localhost:3001/api/chat \ + -H "Content-Type: application/json" \ + -d '{ + "message": "What is the EU AI Act?", + "history": [] + }' +``` + +**JavaScript Example**: +```javascript +const response = await fetch('http://localhost:3001/api/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + message: 'What is the EU AI Act?', + history: [] + }) +}); + +// Read the streaming response +const reader = response.body.getReader(); +const decoder = new TextDecoder(); + +while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + const lines = chunk.split('\n'); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = JSON.parse(line.substring(6)); + console.log(data); + } + } +} +``` + +**Python Example**: +```python +import requests +import json + +response = requests.post( + 'http://localhost:3001/api/chat', + json={ + 'message': 'What is the EU AI Act?', + 'history': [] + }, + stream=True +) + +for line in response.iter_lines(): + if line: + line_str = line.decode('utf-8') + if line_str.startswith('data: '): + data = json.loads(line_str[6:]) + print(data) +``` + +--- + +### 3. Tools Endpoint + +Get a list of available MCP tools. + +**Endpoint**: `GET /api/tools` + +**Response**: +```json +{ + "tools": [ + { + "name": "discover_organization", + "description": "Discover and profile an organization for EU AI Act compliance..." + }, + { + "name": "discover_ai_services", + "description": "Discover and classify AI systems within an organization..." + }, + { + "name": "assess_compliance", + "description": "Assess EU AI Act compliance and generate documentation..." + } + ] +} +``` + +**Example**: +```bash +curl http://localhost:3001/api/tools +``` + +--- + +## Message Types + +### User Message +```typescript +interface UserMessage { + role: "user"; + content: string; +} +``` + +### Assistant Message +```typescript +interface AssistantMessage { + role: "assistant"; + content: string; +} +``` + +### System Message (internal) +```typescript +interface SystemMessage { + role: "system"; + content: string; +} +``` + +--- + +## Tool Schemas + +### discover_organization + +**Description**: Research and profile an organization for EU AI Act compliance. + +**Parameters**: +```typescript +{ + organizationName: string; // Required + domain?: string; // Optional, auto-discovered + context?: string; // Optional, additional context +} +``` + +**Returns**: +```typescript +{ + organization: { + name: string; + sector: string; + size: "SME" | "Large Enterprise" | "Public Body" | "Micro Enterprise"; + aiMaturityLevel: "Nascent" | "Developing" | "Advanced" | "Expert"; + // ... more fields + }, + regulatoryContext: { + applicableFrameworks: string[]; + complianceDeadlines: Array<{...}>; + // ... more fields + }, + metadata: { + completenessScore: number; // 0-100 + // ... more fields + } +} +``` + +### discover_ai_services + +**Description**: Discover and classify AI systems within an organization. + +**Parameters**: +```typescript +{ + organizationContext?: any; // Optional, from discover_organization + systemNames?: string[]; // Optional, specific systems to discover + scope?: string; // Optional: 'all', 'high-risk-only', 'production-only' + context?: string; // Optional, additional context +} +``` + +**Returns**: +```typescript +{ + systems: Array<{ + system: { + name: string; + description: string; + status: "Development" | "Testing" | "Production" | "Deprecated"; + // ... more fields + }, + riskClassification: { + category: "Unacceptable" | "High" | "Limited" | "Minimal"; + riskScore: number; // 0-100 + annexIIICategory?: string; + // ... more fields + }, + complianceStatus: { + // ... compliance fields + } + }>, + riskSummary: { + highRiskCount: number; + limitedRiskCount: number; + // ... more counts + } +} +``` + +### assess_compliance + +**Description**: Assess compliance and generate documentation. + +**Parameters**: +```typescript +{ + organizationContext?: any; // Optional, from discover_organization + aiServicesContext?: any; // Optional, from discover_ai_services + focusAreas?: string[]; // Optional, specific areas to focus on + generateDocumentation?: boolean; // Optional, default: true +} +``` + +**Returns**: +```typescript +{ + assessment: { + overallScore: number; // 0-100 + gaps: Array<{ + area: string; + severity: "Critical" | "High" | "Medium" | "Low"; + article: string; + description: string; + recommendation: string; + }>, + recommendations: Array<{...}> + }, + documentation?: { + riskManagementTemplate: string; // Markdown + technicalDocumentation: string; // Markdown + conformityAssessment: string; // Markdown + transparencyNotice: string; // Markdown + // ... more templates + }, + reasoning: string; // Chain-of-thought explanation +} +``` + +--- + +## Error Handling + +### Common Error Responses + +**400 Bad Request**: +```json +{ + "error": "Message is required" +} +``` + +**500 Internal Server Error**: +```json +{ + "error": "Internal server error", + "message": "Detailed error message" +} +``` + +### Error Types + +1. **Missing Parameters**: 400 error when required parameters are not provided +2. **API Connection**: 500 error if OpenAI API is unreachable +3. **Rate Limiting**: 429 error if rate limits are exceeded +4. **Tool Execution**: 500 error if MCP tools fail + +--- + +## Rate Limiting + +Currently no rate limiting implemented. For production, consider adding: + +```javascript +import rateLimit from 'express-rate-limit'; + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100 // limit each IP to 100 requests per windowMs +}); + +app.use('/api/', limiter); +``` + +--- + +## CORS Configuration + +**Current Setup**: +```javascript +cors({ + origin: ["http://localhost:7860", "http://127.0.0.1:7860"], + credentials: true, +}) +``` + +**For Production**: Configure specific allowed origins: +```javascript +cors({ + origin: ["https://your-gradio-app.com"], + credentials: true, +}) +``` + +--- + +## WebSocket Support + +Currently uses HTTP streaming (SSE). For WebSocket support, add: + +```javascript +import { WebSocketServer } from 'ws'; + +const wss = new WebSocketServer({ server }); + +wss.on('connection', (ws) => { + ws.on('message', async (message) => { + const { content, history } = JSON.parse(message); + // Stream response via WebSocket + for await (const chunk of result.textStream) { + ws.send(JSON.stringify({ type: 'text', content: chunk })); + } + }); +}); +``` + +--- + +## Environment Variables + +Required for API server: + +```bash +# Required +OPENAI_API_KEY=sk-your-openai-api-key + +# Optional +TAVILY_API_KEY=tvly-your-tavily-api-key +PORT=3001 + +# For production +NODE_ENV=production +API_KEY=your-api-authentication-key +ALLOWED_ORIGINS=https://your-app.com +``` + +--- + +## Testing the API + +### Using curl + +**Health check**: +```bash +curl http://localhost:3001/health +``` + +**Simple chat**: +```bash +curl -X POST http://localhost:3001/api/chat \ + -H "Content-Type: application/json" \ + -d '{"message":"What is the EU AI Act?"}' +``` + +**Chat with history**: +```bash +curl -X POST http://localhost:3001/api/chat \ + -H "Content-Type: application/json" \ + -d '{ + "message": "Tell me more", + "history": [ + {"role": "user", "content": "What is the EU AI Act?"}, + {"role": "assistant", "content": "The EU AI Act is..."} + ] + }' +``` + +### Using Postman + +1. Create a new POST request to `http://localhost:3001/api/chat` +2. Set Headers: `Content-Type: application/json` +3. Set Body (raw JSON): +```json +{ + "message": "What is the EU AI Act?", + "history": [] +} +``` +4. Send and view streaming response + +--- + +## Monitoring and Logging + +**Console Logs**: +- All requests are logged to console +- Tool executions are logged +- Errors are logged with stack traces + +**Add Structured Logging**: +```javascript +import winston from 'winston'; + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.json(), + transports: [ + new winston.transports.File({ filename: 'error.log', level: 'error' }), + new winston.transports.File({ filename: 'combined.log' }) + ] +}); + +app.use((req, res, next) => { + logger.info(`${req.method} ${req.url}`); + next(); +}); +``` + +--- + +## Security Best Practices + +1. **Add Authentication**: Use API keys or JWT tokens +2. **Rate Limiting**: Prevent abuse +3. **Input Validation**: Sanitize all inputs +4. **HTTPS**: Use TLS in production +5. **CORS**: Restrict origins +6. **Secrets**: Never commit API keys +7. **Monitoring**: Log all requests and errors + +--- + +## Performance Optimization + +1. **Caching**: Cache organization/system discoveries +```javascript +import NodeCache from 'node-cache'; +const cache = new NodeCache({ stdTTL: 3600 }); +``` + +2. **Compression**: Compress responses +```javascript +import compression from 'compression'; +app.use(compression()); +``` + +3. **Load Balancing**: Use multiple instances +4. **Queuing**: Implement job queue for long tasks + +--- + +## Support + +- 📖 Full documentation: See README.md +- 💬 Issues: GitHub Issues +- 🐛 Bug reports: Include API logs and request details + + diff --git a/apps/eu-ai-act-agent/ARCHITECTURE.md b/apps/eu-ai-act-agent/ARCHITECTURE.md new file mode 100644 index 0000000000000000000000000000000000000000..4b7150d8d79950637144001cf87cae67f6a240f7 --- /dev/null +++ b/apps/eu-ai-act-agent/ARCHITECTURE.md @@ -0,0 +1,674 @@ +# 🏗️ Architecture Documentation + +Detailed technical architecture of the EU AI Act Compliance Agent. + +## System Overview + +``` +┌─────────────────────────────────────────────────────────────┐ +│ CLIENT LAYER │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Gradio Web Interface (Python) │ │ +│ │ - Chat UI with history │ │ +│ │ - Real-time streaming display │ │ +│ │ - Document export features │ │ +│ │ - Status monitoring │ │ +│ └────────────────────┬──────────────────────────────────┘ │ +└─────────────────────────┼──────────────────────────────────┘ + │ HTTP/REST (SSE) + │ +┌─────────────────────────┼──────────────────────────────────┐ +│ API LAYER │ +│ ┌────────────────────┴──────────────────────────────────┐ │ +│ │ Express.js Server (Node.js/TypeScript) │ │ +│ │ - RESTful endpoints │ │ +│ │ - Server-Sent Events (SSE) for streaming │ │ +│ │ - CORS configuration │ │ +│ │ - Request validation │ │ +│ └────────────────────┬──────────────────────────────────┘ │ +└─────────────────────────┼──────────────────────────────────┘ + │ +┌─────────────────────────┼──────────────────────────────────┐ +│ AGENT LAYER │ +│ ┌────────────────────┴──────────────────────────────────┐ │ +│ │ Vercel AI SDK v5 Agent │ │ +│ │ ┌──────────────────────────────────────────────────┐ │ │ +│ │ │ Model: OpenAI gpt-5-chat-latest │ │ │ +│ │ │ - Natural language understanding │ │ │ +│ │ │ - Context management (conversation history) │ │ │ +│ │ │ - Tool calling orchestration │ │ │ +│ │ │ - Streaming response generation │ │ │ +│ │ └──────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌──────────────────────────────────────────────────┐ │ │ +│ │ │ System Prompt │ │ │ +│ │ │ - EU AI Act expert persona │ │ │ +│ │ │ - Tool usage guidelines │ │ │ +│ │ │ - Response formatting rules │ │ │ +│ │ └──────────────────────────────────────────────────┘ │ │ +│ └────────────────────┬──────────────────────────────────┘ │ +└─────────────────────────┼──────────────────────────────────┘ + │ Function Calling + │ +┌─────────────────────────┼──────────────────────────────────┐ +│ TOOL LAYER │ +│ ┌────────────────────┴──────────────────────────────────┐ │ +│ │ MCP Tool Adapters (Vercel AI SDK tool format) │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ 1. discover_organization │ │ │ +│ │ │ - Tavily web research │ │ │ +│ │ │ - Company profiling │ │ │ +│ │ │ - Regulatory mapping │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ 2. discover_ai_services │ │ │ +│ │ │ - AI system discovery │ │ │ +│ │ │ - Risk classification │ │ │ +│ │ │ - Compliance status assessment │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ │ │ │ +│ │ ┌─────────────────────────────────────────────────┐ │ │ +│ │ │ 3. assess_compliance │ │ │ +│ │ │ - Gap analysis (GPT-4) │ │ │ +│ │ │ - Documentation generation │ │ │ +│ │ │ - Recommendations │ │ │ +│ │ └─────────────────────────────────────────────────┘ │ │ +│ └────────────────────┬──────────────────────────────────┘ │ +└─────────────────────────┼──────────────────────────────────┘ + │ +┌─────────────────────────┼──────────────────────────────────┐ +│ EXTERNAL SERVICES │ +│ ┌────────────────────┴──────────────────────────────────┐ │ +│ │ OpenAI API (gpt-5-chat-latest) │ │ +│ │ - Agent intelligence │ │ +│ │ - Compliance assessment │ │ +│ └───────────────────────────────────────────────────────┘ │ +│ │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Tavily API (Optional) │ │ +│ │ - Company research │ │ +│ │ - Web data extraction │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Component Details + +### 1. Gradio Web Interface + +**Technology**: Python 3.9+ with Gradio 5.x + +**Purpose**: Provide user-friendly chat interface for the agent + +**Key Files**: +- `src/gradio_app.py` - Main Gradio application + +**Features**: +- Chat interface with conversation history +- Real-time streaming display +- Status indicator for API connection +- Example queries +- Export functionality (planned) +- Custom EU-themed styling + +**Communication**: HTTP POST requests to Express API with streaming response handling + +**Configuration**: +```python +demo.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + show_error=True, +) +``` + +--- + +### 2. Express API Server + +**Technology**: Node.js 18+ with Express.js and TypeScript + +**Purpose**: REST API layer connecting Gradio to the AI agent + +**Key Files**: +- `src/server.ts` - Express server configuration +- `src/types/index.ts` - TypeScript type definitions + +**Endpoints**: +- `GET /health` - Health check +- `POST /api/chat` - Main chat endpoint (streaming) +- `GET /api/tools` - List available tools + +**Features**: +- CORS configuration for Gradio +- Server-Sent Events (SSE) for streaming +- Request validation +- Error handling +- Logging + +**Configuration**: +```typescript +const app = express(); +app.use(cors({ + origin: ["http://localhost:7860", "http://127.0.0.1:7860"], + credentials: true, +})); +app.use(express.json()); +``` + +--- + +### 3. Vercel AI SDK v5 Agent + +**Technology**: Vercel AI SDK v5 with OpenAI provider + +**Purpose**: Intelligent agent that understands queries and orchestrates tools + +**Key Files**: +- `src/agent/index.ts` - Agent factory and configuration +- `src/agent/prompts.ts` - System prompt and instructions +- `src/agent/tools.ts` - Tool adapters + +**Model**: OpenAI gpt-5-chat-latest +- Context window: 128k tokens +- Supports function calling +- Streaming responses +- Multi-step reasoning + +**Configuration**: +```typescript +const model = openai("gpt-5-chat-latest"); + +streamText({ + model, + messages: [...], + tools: { + discover_organization, + discover_ai_services, + assess_compliance, + }, + maxSteps: 5, // Allow multi-step tool use +}) +``` + +**Capabilities**: +- Natural language understanding +- Intent recognition +- Tool selection and orchestration +- Context management +- Response streaming +- Error handling + +--- + +### 4. MCP Tool Adapters + +**Technology**: Vercel AI SDK `tool()` wrapper + MCP tools + +**Purpose**: Bridge between Vercel AI SDK and MCP tools + +**Key File**: `src/agent/tools.ts` + +**Adapter Pattern**: +```typescript +import { tool } from "ai"; +import { z } from "zod"; +import { mcpToolFunction } from "../../eu-ai-act-mcp/src/tools/..."; + +export const myTool = tool({ + description: "...", + parameters: z.object({...}), + execute: async (params) => { + return await mcpToolFunction(params); + }, +}); +``` + +**Three Tools**: +1. `discover_organization` - Organization profiling +2. `discover_ai_services` - AI system discovery +3. `assess_compliance` - Compliance assessment + +--- + +### 5. MCP Server (Shared) + +**Technology**: Model Context Protocol SDK + +**Purpose**: Reusable compliance tools + +**Location**: `packages/eu-ai-act-mcp/` + +**Integration**: Tools are imported directly by the agent adapters + +**Note**: No separate MCP server process needed for the agent. Tools are used as libraries. + +--- + +## Data Flow + +### Basic Chat Flow + +``` +User Input (Gradio) + ↓ +POST /api/chat {message, history} + ↓ +Express Server validates request + ↓ +Agent.streamText({messages}) + ↓ +gpt-5-chat-latest processes with system prompt + ↓ +Streaming response chunks + ↓ +SSE: data: {type: "text", content: "..."} + ↓ +Gradio displays in real-time +``` + +### Tool Calling Flow + +``` +User: "Analyze OpenAI's compliance" + ↓ +Agent recognizes need for tools + ↓ +Step 1: Call discover_organization("OpenAI") + ├─ Tavily API search + ├─ Data extraction + └─ Return organization profile + ↓ +Step 2: Call discover_ai_services(orgContext) + ├─ System classification + ├─ Risk assessment + └─ Return systems inventory + ↓ +Step 3: Call assess_compliance(org, systems) + ├─ GPT-4 analysis + ├─ Gap identification + └─ Return assessment + docs + ↓ +Agent synthesizes results + ↓ +Stream final response to user +``` + +--- + +## Technology Stack Summary + +| Layer | Technology | Version | Purpose | +|-------|-----------|---------|---------| +| UI | Gradio | 5.9.1+ | Web interface | +| API | Express.js | 4.21+ | REST server | +| Language | TypeScript | 5.9+ | Type safety | +| Agent | Vercel AI SDK | 5.0+ | AI orchestration | +| Model | gpt-5-chat-latest | Latest | Intelligence | +| Tools | MCP SDK | 1.23+ | Tool protocol | +| Research | Tavily | 0.5+ | Web search | +| Validation | Zod | 3.23+ | Schema validation | + +--- + +## Configuration Management + +### Environment Variables + +**Workspace Root** `.env`: +```bash +OPENAI_API_KEY=sk-... +TAVILY_API_KEY=tvly-... +PORT=3001 +``` + +**Loading**: +```typescript +import { config } from "dotenv"; +config({ path: resolve(__dirname, "../../.env") }); +``` + +### Package Configuration + +**Monorepo Structure**: +``` +packages/eu-ai-act-mcp/ # MCP tools +apps/eu-ai-act-agent/ # Agent + UI + ├── src/ + │ ├── server.ts # Express server + │ ├── gradio_app.py # Gradio UI + │ └── agent/ + │ ├── index.ts # Agent config + │ ├── tools.ts # Tool adapters + │ └── prompts.ts # System prompt + └── package.json +``` + +**Dependencies**: +- Agent depends on MCP package +- Imports tools directly (no RPC) +- Shared TypeScript config + +--- + +## Scaling Considerations + +### Horizontal Scaling + +**Current**: Single instance of Express + Gradio + +**For Production**: +1. **Multiple API Instances**: + ``` + Load Balancer + ├─ API Server 1 + ├─ API Server 2 + └─ API Server 3 + ``` + +2. **Session Management**: + - Use Redis for conversation history + - Sticky sessions at load balancer + - Stateless API design + +3. **Gradio Scaling**: + - Multiple Gradio instances + - Shared API endpoint + - CDN for static assets + +### Vertical Scaling + +- Increase Node.js worker threads +- Use clustering module +- Optimize Python workers + +### Caching Strategy + +```typescript +import NodeCache from 'node-cache'; + +const orgCache = new NodeCache({ stdTTL: 3600 }); +const systemCache = new NodeCache({ stdTTL: 1800 }); + +// Cache organization discoveries +if (orgCache.has(orgName)) { + return orgCache.get(orgName); +} +``` + +--- + +## Security Architecture + +### Current State (Development) + +- No authentication +- Open CORS for localhost +- Environment variables for API keys +- No encryption at rest + +### Production Requirements + +1. **Authentication**: +```typescript +import jwt from 'jsonwebtoken'; + +app.use('/api/', (req, res, next) => { + const token = req.headers.authorization; + jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => { + if (err) return res.status(401).send('Unauthorized'); + req.user = decoded; + next(); + }); +}); +``` + +2. **Rate Limiting**: +```typescript +import rateLimit from 'express-rate-limit'; + +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100 +}); +app.use('/api/', limiter); +``` + +3. **Input Validation**: +```typescript +import { z } from 'zod'; + +const ChatRequestSchema = z.object({ + message: z.string().min(1).max(5000), + history: z.array(z.object({ + role: z.enum(['user', 'assistant']), + content: z.string() + })).max(50) +}); +``` + +4. **HTTPS Only**: +```typescript +if (process.env.NODE_ENV === 'production' && !req.secure) { + return res.redirect('https://' + req.headers.host + req.url); +} +``` + +--- + +## Monitoring & Observability + +### Logging + +**Current**: Console logs + +**Recommended**: +```typescript +import winston from 'winston'; + +const logger = winston.createLogger({ + level: 'info', + format: winston.format.json(), + transports: [ + new winston.transports.File({ filename: 'error.log', level: 'error' }), + new winston.transports.File({ filename: 'combined.log' }) + ] +}); +``` + +### Metrics + +Track: +- Request rate +- Response time +- Tool execution time +- Error rate +- OpenAI API usage + +### Alerting + +Monitor: +- API downtime +- High error rates +- OpenAI rate limits +- Disk space (logs) + +--- + +## Development Workflow + +### Local Development + +```bash +# Terminal 1: Watch mode for API +pnpm dev + +# Terminal 2: Python app +python3 src/gradio_app.py + +# Terminal 3: Watch MCP changes +pnpm --filter @eu-ai-act/mcp-server dev +``` + +### Testing + +```bash +# Unit tests (future) +pnpm test + +# Integration tests (future) +pnpm test:integration + +# Manual testing +curl http://localhost:3001/health +``` + +### Building + +```bash +# Build MCP server +pnpm --filter @eu-ai-act/mcp-server build + +# Build agent +pnpm --filter @eu-ai-act/agent build + +# Build all +pnpm build +``` + +--- + +## Deployment Architecture + +### Recommended: Vercel + Hugging Face + +``` +[Vercel] [Hugging Face Spaces] + ↓ ↓ +Express API (Node.js) Gradio UI (Python) + ↓ ↓ +gpt-5-chat-latest + MCP Tools ↓ + ↑ ↓ + ←──────── HTTP/SSE ───────────┘ +``` + +**Benefits**: +- Vercel: Serverless scaling, CDN, automatic HTTPS +- HF Spaces: Free Gradio hosting, GPU access (if needed) +- Separation of concerns + +### Alternative: Single Server + +``` +[VPS / Cloud VM] + ├─ Nginx (reverse proxy) + ├─ Express API :3001 + ├─ Gradio UI :7860 + └─ PM2 (process manager) +``` + +**Benefits**: +- Simpler deployment +- Lower latency (same server) +- Full control + +--- + +## Performance Optimization + +### Response Time + +- **Current**: ~2-5 seconds for simple queries +- **With Tools**: ~10-30 seconds (Tavily + GPT-4 analysis) +- **Optimization**: + - Cache Tavily results (24h TTL) + - Parallel tool execution where possible + - Stream responses immediately + +### Cost Optimization + +- Use gpt-5-chat-latest-mini for simple queries (future) +- Cache frequently requested data +- Batch processing where applicable +- Monitor token usage + +--- + +## Future Enhancements + +1. **WebSocket Support**: Replace SSE with WebSockets +2. **Multi-tenancy**: Support multiple organizations +3. **Persistent Storage**: Database for assessments +4. **Advanced Analytics**: Compliance dashboards +5. **Document Export**: PDF/DOCX generation +6. **Email Reports**: Scheduled compliance reports +7. **API Management**: Rate limiting, quotas, billing +8. **Advanced Caching**: Redis cluster +9. **Internationalization**: Multi-language support +10. **Mobile App**: React Native companion app + +--- + +## Troubleshooting + +### Common Issues + +1. **Agent not responding**: + - Check OPENAI_API_KEY + - Verify API server is running + - Check console for errors + +2. **Tool calls failing**: + - Ensure MCP server is built + - Check tool imports in tools.ts + - Verify environment variables + +3. **Gradio connection issues**: + - Verify API_URL in gradio_app.py + - Check CORS configuration + - Ensure port 3001 is open + +--- + +## Architecture Decisions (ADRs) + +### Why Vercel AI SDK v5? + +- Native streaming support +- Tool calling abstraction +- TypeScript-first +- Active development +- Good documentation + +### Why Gradio? + +- Rapid prototyping +- Built-in chat UI +- Python ecosystem +- Easy deployment (HF Spaces) +- No frontend expertise needed + +### Why Express? + +- Lightweight +- TypeScript support +- Large ecosystem +- Easy to understand +- Flexible + +### Why Direct Tool Import? + +- Simpler architecture +- No RPC overhead +- Shared code between MCP server and agent +- Easier debugging + +--- + +**Questions?** See [README.md](README.md) or [API.md](API.md) + diff --git a/apps/eu-ai-act-agent/DEPLOYMENT.md b/apps/eu-ai-act-agent/DEPLOYMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..e1960a0dde8b8dc671f83bf6c2e2f4b6a0d10837 --- /dev/null +++ b/apps/eu-ai-act-agent/DEPLOYMENT.md @@ -0,0 +1,302 @@ +# 🚀 Deployment Guide + +## Prerequisites + +### System Requirements +- **Node.js** 18+ and pnpm 8+ +- **Python** 3.9+ with uv (fast package manager) +- **Git** for cloning the repository + +### API Keys +1. **OpenAI API Key** (required) + - Sign up at https://platform.openai.com/ + - Create an API key + - Set as `OPENAI_API_KEY` environment variable + +2. **Tavily API Key** (optional, recommended) + - Sign up at https://app.tavily.com + - Get 1,000 free credits/month + - Set as `TAVILY_API_KEY` environment variable + +## Local Development + +### 1. Clone and Install + +```bash +# Clone the repository +git clone +cd mcp-1st-birthday-ai-act + +# Install uv (fast Python package manager) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install Node.js dependencies (from workspace root) +pnpm install + +# Install Python dependencies +cd apps/eu-ai-act-agent +uv pip install -r requirements.txt +``` + +### 2. Configure Environment + +Create `.env` file in the workspace root: + +```bash +# Required +OPENAI_API_KEY=sk-your-openai-api-key + +# Optional (for enhanced organization discovery) +TAVILY_API_KEY=tvly-your-tavily-api-key + +# Server configuration +PORT=3001 +``` + +### 3. Build MCP Server + +The agent depends on the MCP server tools, so build it first: + +```bash +# From workspace root +pnpm --filter @eu-ai-act/mcp-server build +``` + +### 4. Start Development Servers + +**Option A: Run both servers** (recommended) + +Terminal 1 - API Server: +```bash +cd apps/eu-ai-act-agent +pnpm dev +``` + +Terminal 2 - Gradio UI: +```bash +cd apps/eu-ai-act-agent +pnpm gradio +# or: uv run src/gradio_app.py +``` + +**Option B: Use workspace commands** +```bash +# Terminal 1 +pnpm --filter @eu-ai-act/agent dev + +# Terminal 2 +pnpm --filter @eu-ai-act/agent gradio +``` + +### 5. Access the Application + +- **Gradio UI**: http://localhost:7860 +- **API Server**: http://localhost:3001 +- **Health Check**: http://localhost:3001/health + +## Production Deployment + +### Option 1: Vercel (API) + Hugging Face Spaces (Gradio) + +**Deploy API Server to Vercel:** + +1. Create `vercel.json` in `apps/eu-ai-act-agent/`: +```json +{ + "version": 2, + "builds": [ + { + "src": "dist/server.js", + "use": "@vercel/node" + } + ], + "routes": [ + { + "src": "/(.*)", + "dest": "dist/server.js" + } + ], + "env": { + "OPENAI_API_KEY": "@openai-api-key", + "TAVILY_API_KEY": "@tavily-api-key" + } +} +``` + +2. Deploy: +```bash +cd apps/eu-ai-act-agent +pnpm build +vercel --prod +``` + +**Deploy Gradio to Hugging Face Spaces:** + +1. Create a new Space at https://huggingface.co/spaces +2. Choose Gradio SDK +3. Push your code: +```bash +git remote add hf https://huggingface.co/spaces// +git push hf main +``` + +4. Set environment variables in Space settings: + - `API_URL=https://your-vercel-app.vercel.app` + +### Option 2: Docker Compose + +Create `docker-compose.yml`: + +```yaml +version: '3.8' + +services: + api: + build: + context: . + dockerfile: Dockerfile.api + ports: + - "3001:3001" + environment: + - OPENAI_API_KEY=${OPENAI_API_KEY} + - TAVILY_API_KEY=${TAVILY_API_KEY} + restart: unless-stopped + + gradio: + build: + context: . + dockerfile: Dockerfile.gradio + ports: + - "7860:7860" + environment: + - API_URL=http://api:3001 + depends_on: + - api + restart: unless-stopped +``` + +Deploy: +```bash +docker-compose up -d +``` + +### Option 3: Railway / Render + +Both platforms support Node.js and Python apps: + +1. **API Server**: + - Build command: `pnpm build` + - Start command: `pnpm start` + - Add environment variables + +2. **Gradio App**: + - Build command: `curl -LsSf https://astral.sh/uv/install.sh | sh && uv pip install -r requirements.txt` + - Start command: `uv run src/gradio_app.py` + - Set `API_URL` to your API server URL + +## Environment Variables + +### Required +- `OPENAI_API_KEY` - OpenAI API key for GPT-4 (used by agent and assess_compliance tool) + +### Optional +- `TAVILY_API_KEY` - Tavily API key for enhanced organization research +- `PORT` - API server port (default: 3001) +- `API_URL` - Full URL to API server (for Gradio, default: http://localhost:3001) + +## Troubleshooting + +### API Server Issues + +**Problem**: Server won't start +```bash +# Check Node.js version +node --version # Should be 18+ + +# Rebuild dependencies +pnpm install +pnpm --filter @eu-ai-act/mcp-server build +pnpm --filter @eu-ai-act/agent build +``` + +**Problem**: Tools not working +```bash +# Verify MCP server is built +ls packages/eu-ai-act-mcp/dist/ + +# Check environment variables +echo $OPENAI_API_KEY +``` + +### Gradio Issues + +**Problem**: Can't connect to API +- Verify API server is running: `curl http://localhost:3001/health` +- Check `API_URL` in environment or `src/gradio_app.py` + +**Problem**: Python dependencies missing +```bash +# Install uv if not already installed +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install dependencies +uv pip install -r requirements.txt +``` + +### General Issues + +**Problem**: CORS errors +- Ensure Gradio runs on port 7860 (default) +- Check CORS settings in `src/server.ts` + +**Problem**: Rate limits +- OpenAI has rate limits based on your plan +- Consider implementing request queuing or caching + +## Performance Optimization + +1. **Enable Caching**: Add Redis for caching organization/system discoveries +2. **Use Streaming**: Already enabled for real-time responses +3. **Optimize Tools**: Cache Tavily research results +4. **Load Balancing**: Use multiple API server instances behind a load balancer + +## Monitoring + +### Health Checks +```bash +# API health +curl http://localhost:3001/health + +# Tools status +curl http://localhost:3001/api/tools +``` + +### Logging +- API logs: Check console output or configure logging service +- Gradio logs: Built-in console logging +- Consider adding: Sentry, LogRocket, or DataDog + +## Security + +1. **API Keys**: Never commit to Git, use environment variables +2. **CORS**: Restrict origins in production +3. **Rate Limiting**: Add rate limiting middleware +4. **Authentication**: Consider adding API authentication for production +5. **HTTPS**: Always use HTTPS in production + +## Scaling + +For high traffic: +1. Deploy multiple API server instances +2. Use Redis for session management +3. Implement request queuing (Bull/BullMQ) +4. Consider serverless functions for tools +5. Use CDN for static assets + +## Support + +- 📖 Documentation: See README.md +- 🐛 Issues: GitHub Issues +- 💬 Discussions: GitHub Discussions + + diff --git a/apps/eu-ai-act-agent/Dockerfile b/apps/eu-ai-act-agent/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..2a5aedb2a0611360fa2c8cb3801406f08bef7d8e --- /dev/null +++ b/apps/eu-ai-act-agent/Dockerfile @@ -0,0 +1,61 @@ +# EU AI Act Compliance Agent - Hugging Face Spaces +# Deploys Agent + MCP Server from monorepo + +FROM node:20-slim + +# Install Python, pnpm, and uv (to /usr/local/bin for all users) +RUN apt-get update && apt-get install -y \ + python3 \ + python3-venv \ + curl \ + && npm install -g pnpm \ + && curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh \ + && rm -rf /var/lib/apt/lists/* + +# Use existing node user (UID 1000) for HF Spaces compatibility +USER node +ENV HOME=/home/node + +WORKDIR $HOME/app + +# Copy entire monorepo +COPY --chown=node . . + +# Install Node dependencies +RUN pnpm install --frozen-lockfile + +# Build MCP server and Agent +RUN pnpm --filter @eu-ai-act/mcp-server build +RUN pnpm --filter @eu-ai-act/agent build + +# Create Python venv and install dependencies with uv +RUN uv venv $HOME/venv && \ + . $HOME/venv/bin/activate && \ + uv pip install --no-cache -r apps/eu-ai-act-agent/requirements.txt + +# Environment +# API_URL: Internal communication between Gradio and API server (localhost works inside container) +# PUBLIC_URL: External HF Spaces URL for links shown to users +ENV NODE_ENV=production \ + PORT=3001 \ + API_URL=http://localhost:3001 \ + PUBLIC_URL=https://mcp-1st-birthday-eu-ai-act-compliance-agent.hf.space \ + GRADIO_SERVER_NAME=0.0.0.0 \ + GRADIO_SERVER_PORT=7860 \ + GRADIO_SHARE=false \ + PATH=/home/node/venv/bin:$PATH \ + VIRTUAL_ENV=/home/node/venv \ + MCP_SERVER_PATH=/home/node/app/packages/eu-ai-act-mcp/dist/index.js + +# Set working directory to the agent app for CMD +WORKDIR $HOME/app/apps/eu-ai-act-agent + +EXPOSE 7860 + +# Start API server + Main Agent UI (port 7860) +# ChatGPT MCP Server is deployed separately at: +# https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-chatgpt-mcp +CMD node dist/server.js & \ + sleep 2 && \ + python src/gradio_app.py + diff --git a/apps/eu-ai-act-agent/Dockerfile.chatgpt-mcp b/apps/eu-ai-act-agent/Dockerfile.chatgpt-mcp new file mode 100644 index 0000000000000000000000000000000000000000..73d2d0b5f656e4f2dd18686ea82ba4697766d3bf --- /dev/null +++ b/apps/eu-ai-act-agent/Dockerfile.chatgpt-mcp @@ -0,0 +1,57 @@ +# EU AI Act - ChatGPT MCP Server +# Standalone MCP server for ChatGPT Apps integration +# Deploys ONLY the MCP tools (discover_organization, discover_ai_services, assess_compliance) + +FROM node:20-slim + +# Install Python, pnpm, and uv +RUN apt-get update && apt-get install -y \ + python3 \ + python3-venv \ + curl \ + && npm install -g pnpm \ + && curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh \ + && rm -rf /var/lib/apt/lists/* + +# Use existing node user (UID 1000) for HF Spaces compatibility +USER node +ENV HOME=/home/node + +WORKDIR $HOME/app + +# Copy entire monorepo +COPY --chown=node . . + +# Install Node dependencies +RUN pnpm install --frozen-lockfile + +# Build MCP server and Agent (needed for API) +RUN pnpm --filter @eu-ai-act/mcp-server build +RUN pnpm --filter @eu-ai-act/agent build + +# Create Python venv and install dependencies +RUN uv venv $HOME/venv && \ + . $HOME/venv/bin/activate && \ + uv pip install --no-cache -r apps/eu-ai-act-agent/requirements.txt + +# Environment +ENV NODE_ENV=production \ + PORT=3001 \ + API_URL=http://localhost:3001 \ + PUBLIC_URL=https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space \ + CHATGPT_APP_SERVER_NAME=0.0.0.0 \ + CHATGPT_APP_SERVER_PORT=7860 \ + PATH=/home/node/venv/bin:$PATH \ + VIRTUAL_ENV=/home/node/venv \ + MCP_SERVER_PATH=/home/node/app/packages/eu-ai-act-mcp/dist/index.js + +WORKDIR $HOME/app/apps/eu-ai-act-agent + +EXPOSE 7860 + +# Start API server + ChatGPT MCP App on port 7860 +# MCP URL will be: PUBLIC_URL/gradio_api/mcp/ +CMD node dist/server.js & \ + sleep 2 && \ + python src/chatgpt_app.py + diff --git a/apps/eu-ai-act-agent/EXAMPLES.md b/apps/eu-ai-act-agent/EXAMPLES.md new file mode 100644 index 0000000000000000000000000000000000000000..eb4f5989580c90bae5065f8075855c56a1304ff9 --- /dev/null +++ b/apps/eu-ai-act-agent/EXAMPLES.md @@ -0,0 +1,517 @@ +# 💡 Usage Examples + +Real-world examples of using the EU AI Act Compliance Agent. + +## 🎯 Example 1: Understanding the Basics + +### Question +``` +What is the EU AI Act and why should I care about it? +``` + +### Agent Response +The agent will explain: +- Overview of the EU AI Act (Regulation 2024/1689) +- Why it matters for organizations deploying AI in Europe +- Key risk categories and their implications +- Important deadlines (Feb 2025, Aug 2026, Aug 2027) +- Penalties for non-compliance + +**Use Case**: Educating stakeholders and leadership teams + +--- + +## 🏢 Example 2: Organization Discovery + +### Question +``` +Discover and analyze OpenAI's compliance profile +``` + +### Agent Workflow +1. **Calls `discover_organization`** with "OpenAI" + - Searches company information via Tavily + - Identifies sector, size, AI maturity level + - Maps EU presence and regulatory obligations + +2. **Returns Profile**: + ```json + { + "organization": { + "name": "OpenAI", + "sector": "AI Research & Development", + "size": "Large Enterprise", + "aiMaturityLevel": "Expert", + "euPresence": true, + "primaryRole": "Provider" + }, + "regulatoryContext": { + "hasQualityManagementSystem": true, + "hasRiskManagementSystem": true, + "complianceDeadlines": [...] + } + } + ``` + +3. **Provides Insights**: + - OpenAI is a Large Enterprise with Expert AI maturity + - As an AI Provider, they must comply with Articles 16-29 + - They need authorized representative in EU (Article 22) + - Must register high-risk systems in EU database + +**Use Case**: Initial compliance assessment for your organization or analyzing competitors + +--- + +## 🤖 Example 3: AI System Classification + +### Question +``` +Is a recruitment screening AI system high-risk under the EU AI Act? +``` + +### Agent Response +Yes, it's HIGH RISK per **Annex III, Section 4(a)**: "AI systems intended to be used for recruitment or selection of natural persons." + +**Requirements**: +- ✅ Conformity assessment (Article 43) - Must undergo third-party assessment +- ✅ Technical documentation (Article 11, Annex IV) - Comprehensive system docs +- ✅ Risk management system (Article 9) - Continuous risk monitoring +- ✅ Data governance (Article 10) - Training data quality assurance +- ✅ Human oversight (Article 14) - Human-in-the-loop procedures +- ✅ Transparency (Article 13) - Clear information to users +- ✅ CE marking (Article 48) - Affixing CE mark +- ✅ EU database registration (Article 49) - Registration before deployment + +**Timeline**: +- Compliance required by: **August 2, 2026** + +**Use Case**: Determining if your AI system requires strict compliance measures + +--- + +## 🔍 Example 4: Comprehensive System Discovery + +### Question +``` +Scan and classify all AI systems for a company called "TechCorp AI" +``` + +### Agent Workflow +1. **Calls `discover_organization`** for TechCorp AI + - Gets organization context + +2. **Calls `discover_ai_services`** with organization context + - Discovers AI systems (real or mock data) + - Classifies each by risk level + - Identifies compliance gaps + +3. **Returns Inventory**: + ```json + { + "systems": [ + { + "system": { + "name": "Customer Service Chatbot", + "status": "Production" + }, + "riskClassification": { + "category": "Limited", + "riskScore": 35 + } + }, + { + "system": { + "name": "Fraud Detection System", + "status": "Production" + }, + "riskClassification": { + "category": "High", + "annexIIICategory": "Annex III, Section 5(d)", + "riskScore": 85 + } + } + ], + "riskSummary": { + "highRiskCount": 1, + "limitedRiskCount": 1, + "totalCount": 2 + } + } + ``` + +4. **Provides Summary**: + - TechCorp AI has 2 AI systems + - 1 high-risk system requiring immediate attention + - 1 limited-risk system with transparency obligations + +**Use Case**: Creating a comprehensive AI system inventory for compliance planning + +--- + +## 📄 Example 5: Documentation Generation + +### Question +``` +Generate EU AI Act compliance documentation for our customer support chatbot +``` + +### Agent Workflow +1. **Classifies the system** as Limited Risk (Article 50) + +2. **Calls `assess_compliance`** with: + - System type: Customer support chatbot + - Risk category: Limited Risk + - Generate documentation: true + +3. **Generates Templates**: + + **📋 Risk Assessment** + ```markdown + # Risk Assessment: Customer Support Chatbot + + ## Risk Classification + - **Category**: Limited Risk + - **Article**: Article 50 (Transparency Obligations) + + ## Risk Analysis + The chatbot interacts with natural persons and must disclose + that the user is interacting with an AI system... + ``` + + **📄 Technical Documentation** + ```markdown + # Technical Documentation + + ## System Description + - **Name**: Customer Support Chatbot + - **Purpose**: Automated customer service + - **Technology**: Natural Language Processing + + ## Compliance Requirements + 1. Transparency Notice (Article 50)... + ``` + + **📢 Transparency Notice** + ```markdown + # Transparency Notice + + You are interacting with an AI system designed to assist + with customer service inquiries... + ``` + +4. **Provides Export Options**: + - Download as Markdown + - Convert to PDF + - Generate Word document + +**Use Case**: Creating required compliance documentation quickly + +--- + +## ⚖️ Example 6: Compliance Gap Analysis + +### Question +``` +Analyze compliance gaps for our high-risk credit scoring AI +``` + +### Agent Workflow +1. **Calls `discover_ai_services`** with system details + - Classifies as High Risk (Annex III, Section 5(b)) + +2. **Calls `assess_compliance`** + - Analyzes current compliance status + - Identifies gaps + +3. **Returns Gap Analysis**: + ```json + { + "assessment": { + "overallScore": 45, + "gaps": [ + { + "area": "Technical Documentation", + "severity": "Critical", + "article": "Article 11", + "description": "Missing comprehensive technical documentation per Annex IV", + "recommendation": "Create detailed system architecture and data flow documentation" + }, + { + "area": "Conformity Assessment", + "severity": "Critical", + "article": "Article 43", + "description": "No conformity assessment conducted", + "recommendation": "Engage notified body for third-party assessment" + }, + { + "area": "Human Oversight", + "severity": "High", + "article": "Article 14", + "description": "Insufficient human oversight mechanisms", + "recommendation": "Implement human-in-the-loop review process" + } + ], + "recommendations": [...] + } + } + ``` + +4. **Provides Roadmap**: + - Priority 1: Technical documentation + - Priority 2: Conformity assessment + - Priority 3: Human oversight implementation + - Priority 4: Risk management system + +**Use Case**: Understanding what needs to be done to achieve compliance + +--- + +## 🎓 Example 7: Article-Specific Questions + +### Question +``` +Explain Article 14 on human oversight and how to implement it +``` + +### Agent Response +**Article 14: Human Oversight** + +High-risk AI systems must be designed to enable effective oversight by natural persons during operation. + +**Key Requirements**: +1. **Human-in-the-loop** (HITL) - Humans can intervene in real-time +2. **Human-on-the-loop** (HOTL) - Humans can monitor and intervene if needed +3. **Human-in-command** (HIC) - Humans can override AI decisions + +**Implementation Steps**: +1. **Design interfaces** that enable humans to: + - Understand AI outputs + - Interpret system decisions + - Override or reverse decisions + - Interrupt system operation + +2. **Provide training** to oversight personnel on: + - System capabilities and limitations + - Potential risks and biases + - When and how to intervene + +3. **Document procedures** for: + - Escalation paths + - Override protocols + - Incident reporting + +**Example Implementation**: +```typescript +// Human oversight mechanism +interface HumanOversight { + canOverride: boolean; + reviewRequired: boolean; + escalationPath: string[]; + monitoringLevel: "HITL" | "HOTL" | "HIC"; +} +``` + +**Use Case**: Understanding specific compliance requirements in depth + +--- + +## 🌐 Example 8: Multi-Step Analysis + +### Question +``` +I'm launching a new AI-powered hiring platform in the EU. +What do I need to do to be compliant? +``` + +### Agent Workflow (Automatic Multi-Step) + +**Step 1: Risk Classification** +- Identifies this as HIGH RISK (Annex III, Section 4) +- Explains strict requirements apply + +**Step 2: Organization Profile** +- Asks for or discovers organization details +- Determines provider role and obligations + +**Step 3: System Discovery** +- Catalogs the hiring platform features +- Identifies which components are AI-driven + +**Step 4: Compliance Assessment** +- Generates comprehensive compliance checklist +- Identifies documentation requirements +- Provides timeline and budget estimates + +**Step 5: Documentation** +- Generates initial templates for: + - Risk management system + - Technical documentation + - Conformity assessment preparation + - Transparency notices + +**Step 6: Roadmap** +- Creates phased implementation plan +- Suggests notified bodies for assessment +- Provides resource recommendations + +**Use Case**: End-to-end compliance planning for new AI products + +--- + +## 🔄 Example 9: Compliance Monitoring + +### Question +``` +We completed initial compliance last year. +How do we maintain ongoing compliance? +``` + +### Agent Response + +**Ongoing Compliance Requirements**: + +**1. Post-Market Monitoring (Article 72)** +- Establish monitoring system +- Collect and analyze incident data +- Report serious incidents to authorities + +**2. Regular Updates**: +- Update technical documentation with system changes +- Re-assess risks after major updates +- Maintain conformity assessment validity + +**3. Quality Management (Article 17)**: +- Conduct internal audits +- Maintain management review process +- Implement corrective actions + +**4. Record Keeping**: +- Maintain logs for high-risk systems (Article 12) +- Keep documentation for 10 years +- Track all modifications + +**5. Incident Response**: +- Report serious incidents within 15 days +- Implement corrective measures +- Update risk assessments + +**Recommended Schedule**: +- Monthly: Review system logs and incidents +- Quarterly: Internal compliance audit +- Annually: Full compliance review and update documentation +- As needed: Risk re-assessment after changes + +**Use Case**: Establishing continuous compliance processes + +--- + +## 💼 Example 10: Executive Summary + +### Question +``` +Create an executive summary of our AI compliance status +for the board meeting +``` + +### Agent Workflow +1. Runs organization discovery +2. Discovers all AI systems +3. Assesses overall compliance +4. Generates executive summary + +### Sample Output + +```markdown +# EU AI Act Compliance - Executive Summary + +## Overview +- **Organization**: [Company Name] +- **Total AI Systems**: 8 +- **Overall Compliance Score**: 62/100 + +## Risk Breakdown +- 🔴 Unacceptable Risk: 0 systems +- 🟠 High Risk: 2 systems +- 🟡 Limited Risk: 3 systems +- 🟢 Minimal Risk: 3 systems + +## Critical Actions Required +1. **Urgent**: Complete conformity assessment for recruitment AI (Deadline: Aug 2026) +2. **High Priority**: Implement human oversight for credit scoring AI +3. **Medium Priority**: Create transparency notices for chatbots + +## Budget Impact +- Conformity assessments: €50,000 - €100,000 +- Documentation & implementation: €30,000 - €50,000 +- Ongoing compliance: €20,000/year + +## Timeline +- Q1 2025: Complete high-risk system documentation +- Q2 2025: Begin conformity assessments +- Q3 2025: Implement human oversight mechanisms +- Q4 2025: Final compliance verification + +## Risks of Non-Compliance +- Fines up to €35M or 7% of global revenue +- Prohibition from EU market +- Reputational damage +``` + +**Use Case**: Communicating compliance status to leadership and stakeholders + +--- + +## 🎯 Pro Tips for Using the Agent + +### 1. Be Specific +❌ "Tell me about compliance" +✅ "Analyze compliance requirements for our facial recognition system" + +### 2. Provide Context +❌ "Is this high-risk?" +✅ "Is a chatbot for mental health counseling high-risk?" + +### 3. Use Follow-Ups +Ask clarifying questions based on the agent's responses: +- "Can you explain that Article in more detail?" +- "What's the timeline for implementing this?" +- "How much does conformity assessment typically cost?" + +### 4. Request Specifics +- "Generate just the transparency notice" +- "Focus only on Article 10 data governance requirements" +- "What are the penalties for non-compliance?" + +### 5. Leverage Tools +The agent automatically uses the right tools: +- Organization questions → `discover_organization` +- System questions → `discover_ai_services` +- Documentation requests → `assess_compliance` + +--- + +## 📚 Common Questions + +**Q: Can it analyze my actual systems?** +A: With proper integration, yes. Currently it uses mock data but can be connected to your infrastructure. + +**Q: Is the documentation legally binding?** +A: No, it provides templates and guidance. Always consult legal professionals for final documentation. + +**Q: Can it help with GDPR compliance too?** +A: The EU AI Act intersects with GDPR. The agent provides guidance on data governance (Article 10) which aligns with GDPR. + +**Q: How often should I use it?** +A: +- Initially: For assessment and planning +- Quarterly: For compliance reviews +- As needed: When launching new AI systems + +**Q: Can it track multiple projects?** +A: Currently it's conversation-based. Consider exporting and saving assessments for different projects. + +--- + +**Ready to try these examples?** Start the agent and copy any of the questions above! 🚀 + diff --git a/apps/eu-ai-act-agent/QUICKSTART.md b/apps/eu-ai-act-agent/QUICKSTART.md new file mode 100644 index 0000000000000000000000000000000000000000..d9e91fe21ba17ceea88b1d5095e9e47b7491852f --- /dev/null +++ b/apps/eu-ai-act-agent/QUICKSTART.md @@ -0,0 +1,371 @@ +# 🚀 Quick Start Guide + +Get the EU AI Act Compliance Agent running in under 5 minutes! + +## ⚡ Fast Track + +```bash +# 1. Install uv (fast Python package manager) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# 2. Set your API keys (required) +export TAVILY_API_KEY="tvly-your-tavily-key" # Required - Get from https://app.tavily.com +# Choose one model and set its API key: +export ANTHROPIC_API_KEY="sk-ant-your-key" # For Claude 4-5 +# OR +export OPENAI_API_KEY="sk-your-key" # For GPT-5 +# OR +export XAI_API_KEY="xai-your-key" # For Grok 4-1 + +# 3. Install dependencies (from workspace root) +cd /path/to/mcp-1st-birthday-ai-act +pnpm install + +# 4. Install Python packages +cd apps/eu-ai-act-agent +uv pip install -r requirements.txt + +# 5. Start everything (automatic!) +chmod +x start.sh +./start.sh +``` + +That's it! Open http://localhost:7860 🎉 + +## 📋 Step-by-Step Instructions + +### 1. Prerequisites + +Install these first: +- **Node.js 18+**: https://nodejs.org/ +- **pnpm**: `npm install -g pnpm` +- **Python 3.9+**: https://www.python.org/ +- **uv**: https://docs.astral.sh/uv/ (fast Python package manager) +- **Git**: https://git-scm.com/ + +### 2. Get API Keys + +**Tavily (Required)**: +1. Sign up at https://app.tavily.com +2. Get your API key (1,000 free credits/month) +3. Copy it (starts with `tvly-`) + +**Model Selection (Required - Choose One)**: + +**Option A: Claude 4-5 (Anthropic)**: +1. Sign up at https://console.anthropic.com/ +2. Go to API Keys section +3. Create a new key +4. Copy it (starts with `sk-ant-`) + +**Option B: GPT-5 (OpenAI)**: +1. Sign up at https://platform.openai.com/ +2. Go to API Keys section +3. Create a new key +4. Copy it (starts with `sk-`) + +**Option C: Grok 4-1 (xAI)**: +1. Sign up at https://x.ai/ +2. Go to API Keys section +3. Create a new key +4. Copy it (starts with `xai-`) + +### 3. Clone & Setup + +```bash +# Clone the repository +git clone +cd mcp-1st-birthday-ai-act + +# Install Node.js dependencies +pnpm install + +# Install uv (fast Python package manager) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Go to agent directory +cd apps/eu-ai-act-agent + +# Install Python dependencies +uv pip install -r requirements.txt +``` + +### 4. Configure Environment + +Create `.env` file in the **workspace root** (not in apps/eu-ai-act-agent): + +```bash +# Go back to workspace root +cd ../.. + +# Create .env file +cat > .env << EOF +# Required: Tavily API key +TAVILY_API_KEY=tvly-your-tavily-api-key-here + +# Required: Choose one model and provide its API key +# For Claude 4-5: +ANTHROPIC_API_KEY=sk-ant-your-key-here +# OR for GPT-5: +OPENAI_API_KEY=sk-your-openai-api-key-here +# OR for Grok 4-1: +XAI_API_KEY=xai-your-key-here + +PORT=3001 +EOF +``` + +Or copy from example: +```bash +cp .env.example .env +# Then edit .env with your keys +``` + +### 5. Build MCP Server + +The agent needs the MCP server tools: + +```bash +# From workspace root +pnpm --filter @eu-ai-act/mcp-server build +``` + +### 6. Start the Agent + +**Option A: Use startup script** (easiest) +```bash +cd apps/eu-ai-act-agent +chmod +x start.sh +./start.sh +``` + +**Option B: Manual start** (two terminals) + +Terminal 1 - API Server: +```bash +cd apps/eu-ai-act-agent +pnpm dev +``` + +Terminal 2 - Gradio UI: +```bash +cd apps/eu-ai-act-agent +uv run src/gradio_app.py +``` + +**Option C: Use workspace commands** +```bash +# Terminal 1 +pnpm --filter @eu-ai-act/agent dev + +# Terminal 2 +pnpm --filter @eu-ai-act/agent gradio +``` + +### 7. Open the UI + +Navigate to http://localhost:7860 in your browser! + +## 🎯 Try It Out + +### Example 1: General Question +``` +You: What is the EU AI Act? +``` + +The agent will explain the regulation with key details. + +### Example 2: Organization Analysis +``` +You: Analyze OpenAI's EU AI Act compliance +``` + +The agent will: +1. Discover OpenAI's organization profile +2. Identify their AI systems +3. Assess compliance status +4. Provide recommendations + +### Example 3: Risk Classification +``` +You: Is a recruitment screening AI high-risk? +``` + +The agent will classify it per Annex III and explain requirements. + +### Example 4: Documentation +``` +You: Generate compliance documentation for a chatbot +``` + +The agent will create: +- Risk assessment +- Technical documentation +- Transparency notice +- Compliance checklist + +## 🔧 Troubleshooting + +### "Cannot connect to API server" + +**Solution**: +```bash +# Check if API is running +curl http://localhost:3001/health + +# If not, start it: +cd apps/eu-ai-act-agent +pnpm dev +``` + +### "API key error" or "Model not found" + +**Solution**: +```bash +# Verify your Tavily API key is set +echo $TAVILY_API_KEY + +# Verify your model API key is set (check which one you're using) +echo $ANTHROPIC_API_KEY # For Claude 4-5 +echo $OPENAI_API_KEY # For GPT-5 +echo $XAI_API_KEY # For Grok 4-1 + +# Or check .env file +cat ../../.env | grep -E "(TAVILY|ANTHROPIC|OPENAI|XAI)_API_KEY" + +# Make sure your API keys are valid: +# - Tavily: https://app.tavily.com +# - Claude: https://console.anthropic.com/api-keys +# - OpenAI: https://platform.openai.com/api-keys +# - xAI: https://x.ai/api-keys +``` + +### "Module not found" errors + +**Solution**: +```bash +# Reinstall Node.js dependencies +cd /path/to/workspace/root +pnpm install + +# Rebuild MCP server +pnpm --filter @eu-ai-act/mcp-server build + +# Reinstall Python packages +cd apps/eu-ai-act-agent +pip3 install -r requirements.txt +``` + +### Port already in use + +**Solution**: +```bash +# API Server (port 3001) +PORT=3002 pnpm dev + +# Gradio (port 7860) - edit src/gradio_app.py +# Change server_port=7860 to server_port=7861 +``` + +### Python package issues + +**Solution**: +```bash +# Install uv if not already installed +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install dependencies with uv +uv pip install -r requirements.txt + +# Or create and use a virtual environment with uv +uv venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate +uv pip install -r requirements.txt +``` + +## 📊 What's Included + +### Three MCP Tools + +1. **discover_organization** - Profile organizations + - Company research via Tavily + - AI maturity assessment + - Regulatory context + +2. **discover_ai_services** - Catalog AI systems + - Risk classification (Unacceptable/High/Limited/Minimal) + - Compliance status + - Gap analysis + +3. **assess_compliance** - Generate documentation + - Risk management templates + - Technical documentation + - Conformity assessments + - Transparency notices + +### Intelligent Features + +- **Natural Language**: Chat in plain English +- **Contextual**: Remembers conversation history +- **Multi-Step**: Automatically chains tools +- **Streaming**: Real-time responses +- **Export**: Download generated documents + +## 🎓 Learning Path + +1. **Start Simple**: Ask "What is the EU AI Act?" +2. **Try Classification**: "Is [your AI] high-risk?" +3. **Explore Tools**: "Discover [company name]" +4. **Generate Docs**: "Create compliance documentation" +5. **Go Deep**: Ask about specific Articles or requirements + +## 📚 Next Steps + +- **Read the full README**: `cat README.md` +- **Check deployment guide**: `cat DEPLOYMENT.md` +- **Explore the MCP tools**: See `../../packages/eu-ai-act-mcp/README.md` +- **Learn about Vercel AI SDK**: https://ai-sdk.dev/docs +- **Understand Gradio**: https://gradio.app/guides/quickstart + +## 💡 Tips + +1. **Be specific**: "Analyze compliance for our recruitment AI" works better than "check compliance" +2. **Use context**: Mention company names, AI system types, industries +3. **Ask follow-ups**: The agent maintains conversation context +4. **Request docs**: Ask for specific templates or reports +5. **Cite articles**: Reference specific AI Act articles for detailed info + +## 🆘 Getting Help + +- 📖 **Full Documentation**: See README.md +- 🐛 **Found a bug?**: Open a GitHub issue +- 💬 **Questions?**: Check GitHub Discussions +- 📧 **Contact**: See package.json for maintainer info + +## 🎯 Pro Tips + +### For Developers +- Use `pnpm dev` for hot reload during development +- Check `/api/tools` endpoint to see available tools +- API logs show tool execution details +- Gradio supports custom CSS theming + +### For Compliance Teams +- Start with organization discovery +- Document all AI systems systematically +- Focus on high-risk systems first +- Export and archive assessment reports +- Review compliance quarterly + +### For Organizations +- Use as part of compliance workflow +- Train teams on EU AI Act basics +- Generate documentation templates +- Track compliance progress +- Prepare for audits + +--- + +**Ready to go?** Open http://localhost:7860 and start chatting! 🚀 + + diff --git a/apps/eu-ai-act-agent/README.md b/apps/eu-ai-act-agent/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2d94d13eaaf8e5da7fd9e1dae63f35c225eff7e9 --- /dev/null +++ b/apps/eu-ai-act-agent/README.md @@ -0,0 +1,502 @@ +--- +title: EU AI Act Compliance Agent by legitima.ai +emoji: ⚖️ +colorFrom: blue +colorTo: indigo +sdk: docker +pinned: true +tags: + - building-mcp-track-enterprise + - mcp-in-action-track-enterprise + - modal-infernce + - gemini + - claude + - gpt-apps + - gradio-app + - gradio-mcp + - gradio-chatgpt-app + - gpt-oss +short_description: AI-powered EU AI Act compliance assessment with MCP tools +--- + +# 🇪🇺 EU AI Act Compliance Agent by [legitima.ai](https://legitima.ai/mcp-hackathon) powered by [decode](https://decode.gr/en) + +
+ Gradio MCP Hackathon - EU AI Act Compliance +
+ +> **🎂 Built for the MCP 1st Birthday Hackathon** +> **🔗 [Live Demo & Showcase](https://www.legitima.ai/mcp-hackathon)** - See MCP tools and agent capabilities in action! + +An interactive AI agent with Gradio UI for navigating EU AI Act compliance requirements, powered by Vercel AI SDK v5 and the EU AI Act MCP Server. This project demonstrates enterprise-grade MCP tool integration with multi-model AI capabilities for regulatory compliance assessment. + +## 📑 Table of Contents + +- [🎯 Hackathon Submission](#hackathon-submission) +- [🏗️ Architecture](#architecture) +- [🔌 MCP Tools Integration](#mcp-tools-integration) +- [✨ Features](#features) +- [🚀 Getting Started](#getting-started) +- [🚀 How to Use in ChatGPT](#how-to-use-in-chatgpt) +- [📖 Usage Examples](#usage-examples) +- [🔧 Configuration](#configuration) +- [🛠️ Development](#development) +- [📚 API Reference](#api-reference) +- [🧪 Testing](#testing) +- [🎯 Tech Stack](#tech-stack) + + + +## 🎯 Hackathon Submission + +**Track 1: Building MCP** ✅ | **Track 2: MCP in Action** ✅ + +This submission showcases: +- **Custom MCP Server** with 3 specialized tools for EU AI Act compliance +- **Enterprise-grade Agent** using Vercel AI SDK v5 with intelligent tool orchestration +- **ChatGPT Apps Integration** - Deploy as a connector to use tools directly in ChatGPT ([Live MCP Server](https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/)) +- **Multi-model Support** - 6 AI models including free GPT-OSS via Modal.com +- **Real-world Application** - Solving critical regulatory compliance challenges +- **Production-ready Architecture** - Gradio UI + Express API + MCP Protocol + +**🔗 Demo & Showcase:** [www.legitima.ai/mcp-hackathon](https://www.legitima.ai/mcp-hackathon) +**📹 Video:** [Guiddes](https://app.guidde.com/share/playlists/2wXbDrSm2YY7YnWMJbftuu?origin=wywDANMIvNhPu9kYVOXCPpdFcya2) +**📱 Social Media:** [LinkedIn Post 1](https://www.linkedin.com/posts/iordanis-sarafidis_mcp-1st-birthday-mcp-1st-birthday-activity-7400132272282144768-ZIir?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs) + +[LinkedIn Post 2](https://www.linkedin.com/posts/billdrosatos_mcp-1st-birthday-mcp-1st-birthday-activity-7400135422502252544-C5BS?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs) + + + +## 🏗️ Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ Gradio Web UI │ +│ (Python - Interactive Chat Interface) │ +│ Real-time streaming responses │ +└────────────────────┬────────────────────────────────────┘ + │ HTTP/REST + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Express API Server │ +│ (Node.js + Vercel AI SDK v5) │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ AI Agent with Intelligent Tool Orchestration │ │ +│ │ - Multi-model support (6 models) │ │ +│ │ - Streaming responses │ │ +│ │ - Contextual awareness │ │ +│ │ - Automatic tool selection │ │ +│ └─────────────────────────────────────────────────┘ │ +└────────────────────┬────────────────────────────────────┘ + │ MCP Protocol + ▼ +┌─────────────────────────────────────────────────────────┐ +│ EU AI Act MCP Server (@eu-ai-act/mcp) │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Tool 1: discover_organization │ │ +│ │ • Tavily-powered web research │ │ +│ │ • Company profiling & AI maturity │ │ +│ │ • Regulatory context discovery │ │ +│ └─────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Tool 2: discover_ai_services │ │ +│ │ • AI systems inventory │ │ +│ │ • Risk classification (4 tiers) │ │ +│ │ • Compliance status tracking │ │ +│ └─────────────────────────────────────────────────┘ │ +│ ┌─────────────────────────────────────────────────┐ │ +│ │ Tool 3: assess_compliance │ │ +│ │ • AI-powered gap analysis │ │ +│ │ • Multi-model assessment (5 models) │ │ +│ │ • Documentation generation │ │ +│ └─────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + + + +### 🔌 MCP Tools Integration + +This agent leverages a **custom MCP server** (`@eu-ai-act/mcp-server`) that provides three specialized tools for EU AI Act compliance: + +#### 1. `discover_organization` 🏢 +- **Purpose**: Discover and profile organizations for compliance assessment +- **Features**: + - Tavily AI-powered web research for real company data + - AI maturity level assessment (Nascent → Expert) + - Regulatory context discovery (GDPR, ISO certifications) + - EU presence and jurisdiction analysis + - Compliance deadline tracking +- **EU AI Act References**: Articles 16, 17, 22, 49 + +#### 2. `discover_ai_services` 🤖 +- **Purpose**: Inventory and classify AI systems according to EU AI Act risk tiers +- **Features**: + - Automated risk classification (Unacceptable/High/Limited/Minimal) + - Annex III category identification + - Conformity assessment requirements + - Technical documentation status tracking + - Post-market monitoring compliance +- **EU AI Act References**: Articles 6, 9, 10, 11, 12, 14, 43, 47, 48, 49, 72 + +#### 3. `assess_compliance` ⚖️ +- **Purpose**: AI-powered compliance assessment with gap analysis and documentation generation +- **Features**: + - Multi-model AI assessment (Claude 4.5, Claude Opus, GPT-5, Grok 4.1, Gemini 3 Pro) + - Comprehensive gap analysis with Article references + - Priority-based recommendations + - Auto-generated documentation templates: + - Risk Management System (Article 9) + - Technical Documentation (Article 11 / Annex IV) +- **EU AI Act References**: Articles 9-17, 43, 49, 50, Annex IV + +**📚 Full MCP Tools Documentation**: See [`packages/eu-ai-act-mcp/README.md`](../../packages/eu-ai-act-mcp/README.md) for complete tool schemas, input/output formats, and usage examples. + +**💬 Use in ChatGPT**: The MCP server is deployed and ready to use as a ChatGPT App connector at [https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/](https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/) - see [How to Use in ChatGPT](#how-to-use-in-chatgpt) section below for instructions. + + + +## ✨ Features + +### 🤖 Intelligent AI Agent +- **Natural Language Interface**: Ask questions in plain English - no technical knowledge required +- **Contextual Awareness**: Maintains full conversation context throughout the session +- **Multi-Step Workflows**: Automatically orchestrates complex compliance assessments across multiple tools +- **Intelligent Tool Calling**: Seamlessly invokes MCP tools based on user intent and conversation flow +- **Streaming Responses**: Real-time AI responses with tool execution visibility +- **Multi-Model Support**: Choose from 6 AI models including free GPT-OSS (default) + +### 📊 Compliance Capabilities +- **Organization Profiling**: Discover company structure, AI maturity, and regulatory context using Tavily-powered research +- **AI System Discovery**: Catalog and classify all AI systems with automated risk tier assignment +- **Risk Assessment**: Classify systems per EU AI Act (Unacceptable/High/Limited/Minimal) with Article references +- **Gap Analysis**: AI-powered gap identification with severity ratings, remediation effort estimates, and deadlines +- **Documentation Generation**: Auto-generate professional compliance templates (Risk Management, Technical Documentation) +- **Multi-Model Assessment**: Leverage 5 different AI models (Claude, GPT-5, Grok, Gemini) for comprehensive analysis + +### 🎨 Gradio UI +- **Chat Interface**: Clean, modern chat experience +- **Streaming Responses**: Real-time AI responses +- **Document Preview**: View generated compliance documents +- **Export Options**: Download assessment reports and templates +- **Multi-language Support**: Available in multiple EU languages + + + +## 🚀 Getting Started + +### Prerequisites + +- **Node.js** 18+ and pnpm 8+ +- **Python** 3.9+ with uv (fast package manager) +- **Tavily API key** (optional) - Get your free API key from [app.tavily.com](https://app.tavily.com) for enhanced web research +- **Model selection** - Choose one of the following models: + - 🆓 **GPT-OSS 20B** (Modal.com) - **FREE!** ✅ **DEFAULT MODEL** - (⚠️ may take up to 60s to start responding) + - **Claude 4.5 Sonnet** (Anthropic) - `ANTHROPIC_API_KEY` required - Faster & more precise + - **Claude Opus 4** (Anthropic) - `ANTHROPIC_API_KEY` required - Faster & more precise + - **GPT-5** (OpenAI) - `OPENAI_API_KEY` required - Faster & more precise + - **Grok 4.1** (xAI) - `XAI_API_KEY` required - Faster & more precise + - **Gemini 3 Pro** (Google) - `GOOGLE_GENERATIVE_AI_API_KEY` required - Faster & more precise + +### 🆓 Free Default Model: GPT-OSS via Modal.com + +**GPT-OSS 20B is the default model** - no API key required! The agent automatically uses GPT-OSS unless you select a different model in the UI. + +| Feature | Details | +| ----------------- | ---------------------------------------------- | +| **Model** | OpenAI GPT-OSS 20B (open-source) | +| **Cost** | **FREE** (first $30/month on Modal) | +| **Setup** | Just provide Modal endpoint URL | +| **Performance** | ~$0.76/hr when running (A10G GPU) | +| **Response Time** | ⚠️ **May take up to 60s to start** (cold start) | +| **Default** | ✅ **YES** - Automatically selected | + +> ⚠️ **Important:** GPT-OSS may take up to **60 seconds** to start responding due to Modal.com's cold start behavior. For **faster responses and better precision**, select another model (Claude, GPT-5, Gemini, or Grok) and provide your API key in the Gradio UI. + +See [modal/README.md](../../modal/README.md) for detailed deployment instructions and GPU options. + +### Installation + +1. **Install Node.js dependencies**: +```bash +pnpm install +``` + +2. **Install uv and Python dependencies**: +```bash +# Install uv (if not already installed) +curl -LsSf https://astral.sh/uv/install.sh | sh + +# Install Python dependencies +uv pip install -r requirements.txt +``` + +3. **Set up environment variables**: +```bash +cp .env.example .env +# Edit .env and add: +# - MODAL_ENDPOINT_URL (for FREE GPT-OSS - DEFAULT MODEL) - Deploy via: cd modal && modal deploy gpt_oss_inference.py +# - TAVILY_API_KEY (optional) - Get from https://app.tavily.com for enhanced web research +# - Model API key (optional - only if not using GPT-OSS): +# * ANTHROPIC_API_KEY (for Claude 4.5 or Claude Opus) +# * OPENAI_API_KEY (for GPT-5) +# * XAI_API_KEY (for Grok 4.1) +# * GOOGLE_GENERATIVE_AI_API_KEY (for Gemini 3 Pro) +``` + +> 💡 **Tip:** +> - **GPT-OSS is FREE and the default** - just set `MODAL_ENDPOINT_URL` after deploying to Modal.com +> - API keys and Modal endpoint can also be entered directly in the Gradio UI +> - Keys are securely stored in encrypted browser cookies and auto-expire after 24 hours +> - Modal.com offers **$30/month free credit** - perfect for trying out GPT-OSS! + +### Running the Agent + +**Option 1: Run everything together** (recommended) +```bash +# Terminal 1: Start the Express API server +pnpm dev + +# Terminal 2: Start the Gradio UI +pnpm gradio +``` + +**Option 2: Manual start** +```bash +# Terminal 1: Start API server +cd apps/eu-ai-act-agent +pnpm dev + +# Terminal 2: Start Gradio +cd apps/eu-ai-act-agent +uv run src/gradio_app.py +``` + +The Gradio UI will be available at `http://localhost:7860` 🎉 + + + +## 🚀 How to Use in ChatGPT + +The MCP server can be deployed as a **ChatGPT App** (connector) to use EU AI Act compliance tools directly in ChatGPT conversations! + +**🌐 Pre-deployed MCP Server:** The MCP server is already deployed and available at [https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/](https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/) - you can use this URL directly as a ChatGPT connector! + +### Quick Start + +**Option A: Use the Pre-deployed Server** (Recommended) +1. **Use the deployed MCP server** at [https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/](https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/) +2. Skip to step 2 below to configure ChatGPT + +**Option B: Deploy Your Own** +1. **Start the ChatGPT App** with `share=True`: + ```bash + cd apps/eu-ai-act-agent + uv run src/chatgpt_app.py + ``` + + The app will automatically: + - Create a public URL (via Gradio's share feature) + - Enable MCP server mode + - Display the MCP server URL in the terminal + +2. **Enable Developer Mode in ChatGPT**: + - Go to **Settings** → **Apps & Connectors** → **Advanced settings** + - Enable **Developer Mode** + +3. **Create a Connector**: + - In ChatGPT, go to **Settings** → **Apps & Connectors** + - Click **Create Connector** + - Enter the MCP server URL: + - **Pre-deployed:** `https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space/` + - **Or your own:** The URL from the terminal (e.g., `https://xxxxx.gradio.live`) + - Name it `eu-ai-act` (or your preferred name) + +4. **Chat with ChatGPT using the connector**: + - In any ChatGPT conversation, type `@eu-ai-act` to activate the connector + - Ask questions like: + - `@eu-ai-act Analyze OpenAI's EU AI Act compliance status` + - `@eu-ai-act What risk category is a recruitment screening AI?` + - `@eu-ai-act Generate compliance documentation for our chatbot` + +### Available Tools in ChatGPT + +Once connected, you'll have access to all three MCP tools: + +- **`discover_organization`** 🏢 - Discover and profile organizations +- **`discover_ai_services`** 🤖 - Inventory and classify AI systems +- **`assess_compliance`** ⚖️ - AI-powered compliance assessment + +ChatGPT will automatically call these tools based on your conversation context! + + + +## 📖 Usage Examples + +### Example 1: Organization Discovery +``` +You: Analyze OpenAI's EU AI Act compliance status + +Agent: I'll help you assess OpenAI's compliance. Let me start by + discovering their organization profile... + + [Discovering organization details...] + [Analyzing AI systems...] + [Assessing compliance gaps...] + + OpenAI operates as a Large Enterprise with Expert AI maturity. + They have 4 high-risk AI systems requiring conformity assessment... +``` + +### Example 2: AI System Classification +``` +You: What risk category is a recruitment screening AI? + +Agent: Let me check the EU AI Act classification... + + A recruitment screening AI is classified as HIGH RISK per + Annex III, Section 4(a) - AI systems used for recruitment. + + Requirements include: + - Conformity assessment (Article 43) + - Technical documentation (Article 11) + - CE marking (Article 48) + - EU database registration (Article 49) +``` + +### Example 3: Document Generation +``` +You: Generate compliance documentation for our chatbot + +Agent: I'll assess your chatbot and generate the required documents... + + [Generated documents]: + ✓ Risk Management System (Article 9) + ✓ Technical Documentation (Article 11) + + Your chatbot is classified as Limited Risk. The documentation + templates are displayed in the chat and saved to the + compliance-docs directory. +``` + +> ⚠️ **Note on Documentation Generation:** Currently, only **2 documentation templates** are generated: +> - ⚡ **Risk Management System** (Article 9) +> - 📋 **Technical Documentation** (Article 11 / Annex IV) +> +> Additional templates (Conformity Assessment, Transparency Notice, Quality Management System, etc.) are **planned but not yet implemented** to optimize API costs and response speed during the hackathon demo. + + + +## 🔧 Configuration + +### API Server (`src/server.ts`) +- **Port**: Configure via `PORT` env var (default: 3001) +- **Model**: Select between 5 models via UI or `AI_MODEL` env var +- **Streaming**: Enabled for real-time responses +- **CORS**: Configured for Gradio origin +- **Required Environment Variables**: + - `TAVILY_API_KEY` (required for web research) + - One of the following (based on model selection): + - `ANTHROPIC_API_KEY` (for Claude 4.5 or Claude Opus) + - `OPENAI_API_KEY` (for GPT-5) + - `XAI_API_KEY` (for Grok 4.1) + - `GOOGLE_GENERATIVE_AI_API_KEY` (for Gemini 3 Pro) + +### Gradio UI (`src/gradio_app.py`) +- **Theme**: Custom EU-themed design +- **Chat History**: Maintains full conversation context +- **Model Selection**: Dropdown to select AI model in real-time +- **Secure Key Storage**: API keys stored in encrypted browser cookies (24h expiry) +- **Export**: Supports markdown and PDF export (optional) + + + +## 🛠️ Development + +### Project Structure +``` +apps/eu-ai-act-agent/ +├── src/ +│ ├── server.ts # Express API + Vercel AI SDK agent +│ ├── gradio_app.py # Gradio web interface +│ ├── agent/ +│ │ ├── index.ts # Agent configuration +│ │ ├── tools.ts # MCP tool adapters +│ │ └── prompts.ts # System prompts +│ └── types/ +│ └── index.ts # TypeScript types +├── package.json +├── tsconfig.json +└── README.md +``` + +### Building for Production +```bash +# Build the Node.js server +pnpm build + +# Start production server +pnpm start +``` + + + +## 📚 API Reference + +### POST `/api/chat` +Send a chat message to the AI agent. + +**Request:** +```json +{ + "message": "Analyze my organization", + "history": [] +} +``` + +**Response (Stream):** +``` +data: {"type":"text","content":"Let me analyze..."} +data: {"type":"tool_call","tool":"discover_organization"} +data: {"type":"result","data":{...}} +``` + + + +## 🧪 Testing + +Test the agent with sample queries: +```bash +curl -X POST http://localhost:3001/api/chat \ + -H "Content-Type: application/json" \ + -d '{"message":"What is the EU AI Act?"}' +``` + + + +## 🎯 Tech Stack + +- **Backend**: Node.js + Express + TypeScript +- **AI SDK**: Vercel AI SDK v5 (upgraded from v4) +- **LLM**: 6 models supported (user selectable via UI): + - 🆓 **GPT-OSS 20B** (Modal.com) - **FREE!** ✅ **DEFAULT MODEL** - No API key required! (⚠️ may take up to 60s to start) + - Claude 4.5 Sonnet & Claude Opus 4 (Anthropic) - Faster & more precise + - GPT-5 (OpenAI) - Faster & more precise + - Grok 4.1 (xAI) - Faster & more precise + - Gemini 3 Pro (Google) - Faster & more precise +- **Free LLM Hosting**: [Modal.com](https://modal.com) for GPT-OSS deployment +- **Research**: Tavily AI for web research (optional) +- **Frontend**: Gradio (Python) +- **Security**: Encrypted cookie storage for API keys (24h expiry) +- **MCP**: Model Context Protocol for tool integration +- **Monorepo**: Turborepo for efficient builds + + +
+ +**Built for the MCP 1st Birthday Hackathon** 🎂 + +Making EU AI Act compliance accessible through conversational AI + +
+ diff --git a/apps/eu-ai-act-agent/biome.json b/apps/eu-ai-act-agent/biome.json new file mode 100644 index 0000000000000000000000000000000000000000..423ce54402ec0658a69a200801cf49affafd3fef --- /dev/null +++ b/apps/eu-ai-act-agent/biome.json @@ -0,0 +1,30 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "space" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/apps/eu-ai-act-agent/package.json b/apps/eu-ai-act-agent/package.json new file mode 100644 index 0000000000000000000000000000000000000000..ed19d43bd3288d62418853f46dde3d294fc4e5a5 --- /dev/null +++ b/apps/eu-ai-act-agent/package.json @@ -0,0 +1,47 @@ +{ + "name": "@eu-ai-act/agent", + "version": "0.1.0", + "description": "EU AI Act Compliance Agent with Gradio UI and Vercel AI SDK v5", + "type": "module", + "private": true, + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsup", + "start": "node dist/server.js", + "typecheck": "tsc --noEmit", + "lint": "biome check .", + "lint:fix": "biome check --write .", + "gradio": "uv run src/gradio_app.py", + "chatgpt-app": "uv run src/chatgpt_app.py" + }, + "dependencies": { + "@ai-sdk/anthropic": "^2.0.50", + "@ai-sdk/google": "3.0.0-beta.62", + "@ai-sdk/mcp": "^0.0.11", + "@ai-sdk/openai": "^2.0.74", + "@ai-sdk/provider-utils": "^2.2.8", + "@ai-sdk/xai": "^2.0.39", + "@eu-ai-act/mcp-server": "workspace:*", + "@modelcontextprotocol/sdk": "^1.23.0", + "ai": "^5.0.104", + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^4.21.2", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/cors": "^2.8.17", + "@types/express": "^5.0.5", + "@types/node": "^22.10.2", + "tsup": "^8.5.1", + "tsx": "^4.20.6", + "typescript": "^5.9.3" + }, + "keywords": [ + "eu-ai-act", + "compliance", + "ai-agent", + "gradio", + "vercel-ai-sdk" + ] +} diff --git a/apps/eu-ai-act-agent/pyproject.toml b/apps/eu-ai-act-agent/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..407154dbb1761606bf9e9e469fa3a225cfe70ce5 --- /dev/null +++ b/apps/eu-ai-act-agent/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "eu-ai-act-agent" +version = "0.1.0" +description = "EU AI Act Compliance Agent with Gradio UI" +requires-python = ">=3.10" +dependencies = [ + "gradio[mcp]>=6.0.0", + "requests>=2.31.0", + "python-dotenv>=1.0.0", +] + +[project.scripts] +gradio = "gradio_app:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src"] + +[tool.uv] +dev-dependencies = [] + diff --git a/apps/eu-ai-act-agent/requirements.txt b/apps/eu-ai-act-agent/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..e85a2b2a191a723cebec8ef5b949892039482f0c --- /dev/null +++ b/apps/eu-ai-act-agent/requirements.txt @@ -0,0 +1,7 @@ +# EU AI Act Compliance Agent - Python Dependencies +# Gradio UI requirements + +gradio[mcp]>=6.0.0 +requests>=2.31.0 +python-dotenv>=1.0.0 + diff --git a/apps/eu-ai-act-agent/src/.mcp_url b/apps/eu-ai-act-agent/src/.mcp_url new file mode 100644 index 0000000000000000000000000000000000000000..dcb8452054100f8ccc508f03c34d368f1248345c --- /dev/null +++ b/apps/eu-ai-act-agent/src/.mcp_url @@ -0,0 +1 @@ +https://c93e68e71ab8607092.gradio.live/gradio_api/mcp/ \ No newline at end of file diff --git a/apps/eu-ai-act-agent/src/agent/index.ts b/apps/eu-ai-act-agent/src/agent/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..6a941625fc66dbd6615b55f38426e172bc481040 --- /dev/null +++ b/apps/eu-ai-act-agent/src/agent/index.ts @@ -0,0 +1,819 @@ +/** + * EU AI Act Compliance Agent + * Vercel AI SDK v5 implementation with MCP tools + * + * Uses the AI SDK MCP client to connect to the EU AI Act MCP server + * and retrieve tools dynamically. + * + * IMPORTANT: API keys are passed directly from Gradio UI via request headers. + * NEVER read API keys from environment variables! + * + * Supported Models: + * - gpt-oss: OpenAI GPT-OSS 20B via Modal.com (FREE - no API key needed!) - DEFAULT + * - claude-4.5: Anthropic Claude Sonnet 4.5 (user provides API key) + * - claude-opus: Anthropic Claude Opus 4 (user provides API key) + * - gpt-5: OpenAI GPT-5 (user provides API key) + * - grok-4-1: xAI Grok 4.1 Fast Reasoning (user provides API key) + * - gemini-3: Google Gemini 3 Pro (user provides API key) + */ + +import { generateText, stepCountIs, streamText } from "ai"; +import { experimental_createMCPClient as createMCPClient } from "@ai-sdk/mcp"; +import { Experimental_StdioMCPTransport as StdioMCPTransport } from "@ai-sdk/mcp/mcp-stdio"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { SYSTEM_PROMPT } from "./prompts.js"; +import { getModel, type ApiKeys } from "@eu-ai-act/mcp-server"; + +// Re-export ApiKeys type for server.ts +export type { ApiKeys }; + +/** + * Agent configuration passed from server + */ +export interface AgentConfig { + modelName: string; + apiKeys: ApiKeys; + tavilyApiKey?: string; +} + +/** + * Get the system prompt for the agent + */ +function getSystemPrompt(): string { + return SYSTEM_PROMPT; +} + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Get path to the MCP server + * + * In production (Docker/HF Spaces): Use MCP_SERVER_PATH env var + * In development (tsx watch): Calculate relative path from source + * In local production (node dist/server.js): Calculate relative path from dist + */ +function getMCPServerPath(): string { + // 1. Use environment variable if set (for Docker/production deployments) + if (process.env.MCP_SERVER_PATH) { + console.log( + `[Agent] Using MCP_SERVER_PATH from env: ${process.env.MCP_SERVER_PATH}`, + ); + return process.env.MCP_SERVER_PATH; + } + + // 2. Calculate relative path based on whether we're running from dist or src + // - From src/agent/index.ts: need to go up 4 levels (agent -> src -> eu-ai-act-agent -> apps -> root) + // - From dist/server.js: need to go up 3 levels (dist -> eu-ai-act-agent -> apps -> root) + const isRunningFromDist = + __dirname.includes("/dist") || __dirname.endsWith("/dist"); + const levelsUp = isRunningFromDist ? "../../../" : "../../../../"; + const relativePath = resolve( + __dirname, + levelsUp, + "packages/eu-ai-act-mcp/dist/index.js", + ); + + console.log( + `[Agent] MCP server path (${isRunningFromDist ? "dist" : "src"}): ${relativePath}`, + ); + return relativePath; +} + +// Path to the built MCP server +const MCP_SERVER_PATH = getMCPServerPath(); + +/** + * HIGH-RISK KEYWORDS based on EU AI Act Annex III + * Source: https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=OJ:L_202401689 + */ +const HIGH_RISK_KEYWORDS = [ + // Annex III Point 8(a) - Administration of justice (LEGAL AI) + "legal", + "law", + "lawyer", + "attorney", + "judicial", + "justice", + "court", + "litigation", + "contract", + "compliance", + "regulatory", + "statute", + "legal advice", + "legal consulting", + "legal assistant", + "legal research", + "dispute resolution", + "arbitration", + "mediation", + // Annex III Point 4 - Employment + "recruitment", + "hiring", + "hr", + "human resources", + "employee", + "workforce", + "resume", + "cv", + "candidate", + "job application", + "termination", + // Annex III Point 5 - Essential services + "credit", + "scoring", + "loan", + "insurance", + "financial risk", + "creditworthiness", + "emergency services", + // Annex III Point 1 - Biometrics + "biometric", + "facial recognition", + "face recognition", + "fingerprint", + "identity verification", + "remote identification", + // Annex III Point 3 - Education + "education", + "student", + "academic", + "exam", + "grading", + "admission", + // Annex III Point 6 - Law enforcement + "law enforcement", + "police", + "crime", + "profiling", + "polygraph", + // Annex III Point 2 - Critical infrastructure + "critical infrastructure", + "safety component", + "water supply", + "gas supply", + "electricity", + "transport", + // Annex III Point 5(b) - Healthcare + "healthcare", + "medical", + "diagnosis", + "clinical", + "patient", + "health", +]; + +/** + * Validate risk classification for a system based on EU AI Act Annex III + * Ensures legal AI systems are correctly classified as HIGH RISK per Point 8(a) + */ +function validateSystemRiskClassification(system: any): any { + if (!system) return system; + + const name = (system.system?.name || system.name || "").toLowerCase(); + const description = ( + system.system?.description || + system.description || + "" + ).toLowerCase(); + const purpose = ( + system.system?.intendedPurpose || + system.intendedPurpose || + "" + ).toLowerCase(); + const contextString = `${name} ${description} ${purpose}`; + + // Check for legal AI indicators (Annex III Point 8(a)) + const isLegalAI = + contextString.includes("legal") || + contextString.includes("law") || + contextString.includes("lawyer") || + contextString.includes("attorney") || + contextString.includes("judicial") || + contextString.includes("justice") || + contextString.includes("court") || + contextString.includes("litigation") || + contextString.includes("contract review") || + contextString.includes("compliance advi") || + contextString.includes("regulatory advi"); + + // Check for other high-risk keywords + const matchedHighRiskKeywords = HIGH_RISK_KEYWORDS.filter((keyword) => + contextString.includes(keyword.toLowerCase()), + ); + const hasHighRiskKeywords = matchedHighRiskKeywords.length > 0; + + const rc = system.riskClassification || {}; + let needsCorrection = false; + + if (isLegalAI && rc.category !== "High" && rc.category !== "Unacceptable") { + console.log( + `⚠️ [Agent Risk Validation] Legal AI detected - correcting "${rc.category}" to "High"`, + ); + console.log(` System: ${name}`); + console.log( + ` Reason: Legal AI per EU AI Act Annex III Point 8(a) - Administration of justice`, + ); + rc.category = "High"; + rc.annexIIICategory = + "Annex III, Point 8(a) - Administration of justice and democratic processes"; + rc.justification = + "AI system providing legal assistance, consulting, or advice. Per EU AI Act Annex III Point 8(a), such systems are HIGH RISK."; + rc.riskScore = Math.max(rc.riskScore || 0, 85); + rc.conformityAssessmentRequired = true; + rc.conformityAssessmentType = "Internal Control"; + needsCorrection = true; + } else if ( + hasHighRiskKeywords && + rc.category !== "High" && + rc.category !== "Unacceptable" + ) { + console.log( + `⚠️ [Agent Risk Validation] High-risk keywords detected - correcting "${rc.category}" to "High"`, + ); + console.log(` System: ${name}`); + console.log( + ` Keywords: ${matchedHighRiskKeywords.slice(0, 3).join(", ")}`, + ); + rc.category = "High"; + rc.riskScore = Math.max(rc.riskScore || 0, 75); + rc.conformityAssessmentRequired = true; + rc.conformityAssessmentType = "Internal Control"; + needsCorrection = true; + } + + if (needsCorrection) { + return { + ...system, + riskClassification: rc, + }; + } + + return system; +} + +/** + * Validate all systems in assess_compliance or discover_ai_services results + */ +function validateToolResult(toolName: string, result: any): any { + if (!result) return result; + + if (toolName === "assess_compliance" || toolName === "discover_ai_services") { + // Check if result has systems array + if (result.systems && Array.isArray(result.systems)) { + result.systems = result.systems.map((s: any) => + validateSystemRiskClassification(s), + ); + + // Recalculate risk summary + if (result.riskSummary) { + result.riskSummary = { + ...result.riskSummary, + highRiskCount: result.systems.filter( + (s: any) => s.riskClassification?.category === "High", + ).length, + limitedRiskCount: result.systems.filter( + (s: any) => s.riskClassification?.category === "Limited", + ).length, + minimalRiskCount: result.systems.filter( + (s: any) => s.riskClassification?.category === "Minimal", + ).length, + unacceptableRiskCount: result.systems.filter( + (s: any) => s.riskClassification?.category === "Unacceptable", + ).length, + }; + } + } + + // Check assessment for gaps related to legal systems + if (result.assessment?.gaps) { + // Ensure legal AI systems have appropriate gaps flagged + } + } + + return result; +} + +// getModel is now imported from @eu-ai-act/mcp-server + +/** + * Create MCP client and retrieve tools + * Passes API keys to the MCP server for tool execution + */ +async function createMCPClientWithTools( + apiKeys: ApiKeys, + modelName: string, + tavilyApiKey?: string, +) { + // Pass API keys to MCP server child process via environment + // MCP tools need these for Tavily research and AI model calls + // IMPORTANT: These come from Gradio UI user input - NEVER from process.env! + const env: Record = { + // Only pass MCP_SERVER_PATH and NODE_ENV, plus API keys + NODE_ENV: process.env.NODE_ENV || "production", + }; + + // Pass MCP server path if set + if (process.env.MCP_SERVER_PATH) { + env.MCP_SERVER_PATH = process.env.MCP_SERVER_PATH; + } + + // Pass model name from Gradio UI + env.AI_MODEL = modelName; + + // Pass Tavily API key: User-provided (from Gradio UI) takes priority, + // otherwise fallback to server's env var (HF Spaces secret) + if (tavilyApiKey) { + env.TAVILY_API_KEY = tavilyApiKey; + console.log("[Agent] Using Tavily API key from Gradio UI (user-provided)"); + } else if (process.env.TAVILY_API_KEY) { + // Fallback to server's TAVILY_API_KEY (HF Spaces secret) + env.TAVILY_API_KEY = process.env.TAVILY_API_KEY; + console.log( + "[Agent] Using Tavily API key from server env (HF Spaces secret)", + ); + } else { + console.log( + "[Agent] No Tavily API key available - will use AI model fallback for research", + ); + } + + // Pass API keys from user (via Gradio UI) to MCP server + // These are used by MCP tools for Tavily research and AI model calls + if (apiKeys.openaiApiKey) env.OPENAI_API_KEY = apiKeys.openaiApiKey; + if (apiKeys.anthropicApiKey) env.ANTHROPIC_API_KEY = apiKeys.anthropicApiKey; + if (apiKeys.googleApiKey) + env.GOOGLE_GENERATIVE_AI_API_KEY = apiKeys.googleApiKey; + if (apiKeys.xaiApiKey) env.XAI_API_KEY = apiKeys.xaiApiKey; + if (apiKeys.modalEndpointUrl) + env.MODAL_ENDPOINT_URL = apiKeys.modalEndpointUrl; + + const transport = new StdioMCPTransport({ + command: "node", + args: [MCP_SERVER_PATH], + env, + }); + + const client = await createMCPClient({ transport }); + const tools = await client.tools(); + + console.log("[Agent] MCP client created"); + + return { client, tools }; +} + +/** + * Create EU AI Act compliance agent + * + * @param config - Agent configuration with model name and API keys from Gradio UI + */ +export function createAgent(config: AgentConfig) { + const { modelName, apiKeys, tavilyApiKey } = config; + + // Log the model being used + console.log(`[Agent] Creating agent with model: ${modelName}`); + const model = getModel(modelName, apiKeys, "agent"); + console.log(`[Agent] Model instance created: ${model.constructor.name}`); + return { + /** + * Generate a single response + */ + async generateText(params: { messages: any[] }) { + const { client, tools } = await createMCPClientWithTools( + apiKeys, + modelName, + tavilyApiKey, + ); + + try { + const systemPrompt = getSystemPrompt(); + const isGptOss = modelName.toLowerCase().includes("gpt-oss"); + + // Build provider options based on model + // GPT-OSS (vLLM on Modal) doesn't support reasoningEffort parameter + const providerOptions = isGptOss + ? undefined + : { + anthropic: { + thinking: { type: "enabled", budgetTokens: 2000 }, // Minimal thinking budget for Claude + }, + openai: { + reasoningEffort: "low", // Low reasoning effort for GPT - faster responses + }, + google: { + thinkingConfig: { + thinkingLevel: "low", // Low thinking for faster responses + includeThoughts: true, + }, + }, + }; + + const result = await generateText({ + model, + messages: [ + { role: "system", content: systemPrompt }, + ...params.messages, + ], + // MCP tools are compatible at runtime but have different TypeScript types + tools: tools as any, + // stop when at least three tools runned and response is generated + stopWhen: stepCountIs(3), + providerOptions, + }); + + // Output tool results with detailed information + if (result.steps) { + for (const step of result.steps) { + if (step.toolResults && step.toolResults.length > 0) { + for (const toolResult of step.toolResults) { + console.log(`\n📋 Tool Result: ${toolResult.toolName}`); + console.log("─".repeat(50)); + + try { + // Access result safely - TypedToolResult may have result in different property + const resultValue = + (toolResult as any).result ?? + (toolResult as any).output ?? + toolResult; + let parsed = + typeof resultValue === "string" + ? JSON.parse(resultValue) + : resultValue; + + // Validate and correct risk classifications per EU AI Act Annex III + parsed = validateToolResult(toolResult.toolName, parsed); + + // Handle assess_compliance results specially to show documentation + if (toolResult.toolName === "assess_compliance" && parsed) { + if (parsed.assessment) { + console.log( + `📊 Compliance Score: ${parsed.assessment.overallScore}/100`, + ); + console.log( + `⚠️ Risk Level: ${parsed.assessment.riskLevel}`, + ); + console.log( + `🔍 Gaps Found: ${parsed.assessment.gaps?.length || 0}`, + ); + console.log( + `💡 Recommendations: ${parsed.assessment.recommendations?.length || 0}`, + ); + } + + // Show documentation templates + if (parsed.documentation) { + console.log(`\n📄 Documentation Templates Generated:`); + const docs = parsed.documentation; + if (docs.riskManagementTemplate) + console.log(" ✓ Risk Management System (Article 9)"); + if (docs.technicalDocumentation) + console.log( + " ✓ Technical Documentation (Article 11)", + ); + if (docs.conformityAssessment) + console.log(" ✓ Conformity Assessment (Article 43)"); + if (docs.transparencyNotice) + console.log(" ✓ Transparency Notice (Article 50)"); + if (docs.qualityManagementSystem) + console.log( + " ✓ Quality Management System (Article 17)", + ); + if (docs.humanOversightProcedure) + console.log( + " ✓ Human Oversight Procedure (Article 14)", + ); + if (docs.dataGovernancePolicy) + console.log(" ✓ Data Governance Policy (Article 10)"); + if (docs.incidentReportingProcedure) + console.log(" ✓ Incident Reporting Procedure"); + } + + // Show documentation files + if ( + parsed.metadata?.documentationFiles && + parsed.metadata.documentationFiles.length > 0 + ) { + console.log(`\n💾 Documentation Files Saved:`); + for (const filePath of parsed.metadata + .documentationFiles) { + console.log(` 📄 ${filePath}`); + } + } + + if (parsed.metadata) { + console.log( + `\n🤖 Model Used: ${parsed.metadata.modelUsed}`, + ); + } + } else if ( + toolResult.toolName === "discover_ai_services" && + parsed + ) { + // Validate systems in discovery results + if (parsed.riskSummary) { + console.log( + `🤖 Total Systems: ${parsed.riskSummary.totalCount}`, + ); + console.log( + ` High-Risk: ${parsed.riskSummary.highRiskCount}`, + ); + console.log( + ` Limited-Risk: ${parsed.riskSummary.limitedRiskCount}`, + ); + console.log( + ` Minimal-Risk: ${parsed.riskSummary.minimalRiskCount}`, + ); + } + // Show any legal AI systems detected + if (parsed.systems) { + const legalSystems = parsed.systems.filter((s: any) => { + const ctx = + `${s.system?.name || ""} ${s.system?.intendedPurpose || ""}`.toLowerCase(); + return ( + ctx.includes("legal") || + ctx.includes("law") || + ctx.includes("judicial") + ); + }); + if (legalSystems.length > 0) { + console.log( + `\n⚖️ Legal AI Systems (HIGH RISK per Annex III Point 8(a)):`, + ); + for (const sys of legalSystems) { + console.log( + ` - ${sys.system?.name}: ${sys.riskClassification?.category}`, + ); + } + } + } + } else if ( + toolResult.toolName === "discover_organization" && + parsed + ) { + if (parsed.organization) { + console.log( + `🏢 Organization: ${parsed.organization.name}`, + ); + console.log(`📍 Sector: ${parsed.organization.sector}`); + console.log( + `🌍 EU Presence: ${parsed.organization.euPresence}`, + ); + } + } + } catch { + // If not JSON, just show raw result summary + const resultValue = + (toolResult as any).result ?? + (toolResult as any).output ?? + toolResult; + const resultStr = String(resultValue); + console.log( + `Result: ${resultStr.substring(0, 200)}${resultStr.length > 200 ? "..." : ""}`, + ); + } + + console.log("─".repeat(50)); + } + } + } + } + + return result; + } finally { + await client.close(); + } + }, + + /** + * Stream a response with MCP tools + */ + async streamText(params: { messages: any[] }) { + const { client, tools } = await createMCPClientWithTools( + apiKeys, + modelName, + tavilyApiKey, + ); + const systemPrompt = getSystemPrompt(); + const isGptOss = modelName.toLowerCase().includes("gpt-oss"); + + // Build provider options based on model + // GPT-OSS (vLLM on Modal) doesn't support reasoningEffort parameter + const providerOptions = isGptOss + ? undefined + : { + anthropic: { + thinking: { type: "enabled", budgetTokens: 2000 }, // Minimal thinking budget for Claude + }, + openai: { + reasoningEffort: "low", // Low reasoning effort for GPT - faster responses + }, + google: { + thinkingConfig: { + thinkingLevel: "low", // Low thinking for faster responses + includeThoughts: true, + }, + }, + }; + + // For GPT-OSS (vLLM on Modal), we must set explicit maxOutputTokens + // The context window is 16k, and the system prompt + tools take ~6-8k tokens + // Setting explicit maxOutputTokens prevents vLLM from calculating negative values + // 8000 tokens allows for comprehensive compliance reports + const maxOutputTokens = isGptOss ? 8000 : undefined; + + const result = streamText({ + model, + maxOutputTokens, + messages: [ + { role: "system", content: systemPrompt }, + ...params.messages, + ], + // MCP tools are compatible at runtime but have different TypeScript types + tools: tools as any, + // Increase maxSteps to ensure all 3 tools + final response + stopWhen: stepCountIs(3), + providerOptions, + // Log each step for debugging + onStepFinish: async (step) => { + // Output tool results with detailed information + if (step.toolResults && step.toolResults.length > 0) { + for (const toolResult of step.toolResults) { + console.log(`\n📋 Tool Result: ${toolResult.toolName}`); + console.log("─".repeat(50)); + + try { + // Access result safely - TypedToolResult may have result in different property + const resultValue = + (toolResult as any).result ?? + (toolResult as any).output ?? + toolResult; + let result = + typeof resultValue === "string" + ? JSON.parse(resultValue) + : resultValue; + + // Validate and correct risk classifications per EU AI Act Annex III + result = validateToolResult(toolResult.toolName, result); + + // Handle assess_compliance results specially to show documentation + if (toolResult.toolName === "assess_compliance" && result) { + if (result.assessment) { + console.log( + `📊 Compliance Score: ${result.assessment.overallScore}/100`, + ); + console.log( + `⚠️ Risk Level: ${result.assessment.riskLevel}`, + ); + console.log( + `🔍 Gaps Found: ${result.assessment.gaps?.length || 0}`, + ); + console.log( + `💡 Recommendations: ${result.assessment.recommendations?.length || 0}`, + ); + } + + // Show documentation templates + if (result.documentation) { + console.log(`\n📄 Documentation Templates Generated:`); + const docs = result.documentation; + if (docs.riskManagementTemplate) + console.log(" ✓ Risk Management System (Article 9)"); + if (docs.technicalDocumentation) + console.log(" ✓ Technical Documentation (Article 11)"); + if (docs.conformityAssessment) + console.log(" ✓ Conformity Assessment (Article 43)"); + if (docs.transparencyNotice) + console.log(" ✓ Transparency Notice (Article 50)"); + if (docs.qualityManagementSystem) + console.log( + " ✓ Quality Management System (Article 17)", + ); + if (docs.humanOversightProcedure) + console.log( + " ✓ Human Oversight Procedure (Article 14)", + ); + if (docs.dataGovernancePolicy) + console.log(" ✓ Data Governance Policy (Article 10)"); + if (docs.incidentReportingProcedure) + console.log(" ✓ Incident Reporting Procedure"); + } + + // Show documentation files + if ( + result.metadata?.documentationFiles && + result.metadata.documentationFiles.length > 0 + ) { + console.log(`\n💾 Documentation Files Saved:`); + for (const filePath of result.metadata.documentationFiles) { + console.log(` 📄 ${filePath}`); + } + } + + if (result.metadata) { + console.log( + `\n🤖 Model Used: ${result.metadata.modelUsed}`, + ); + } + } else if ( + toolResult.toolName === "discover_ai_services" && + result + ) { + // Validate systems in discovery results + if (result.riskSummary) { + console.log( + `🤖 Total Systems: ${result.riskSummary.totalCount}`, + ); + console.log( + ` High-Risk: ${result.riskSummary.highRiskCount}`, + ); + console.log( + ` Limited-Risk: ${result.riskSummary.limitedRiskCount}`, + ); + console.log( + ` Minimal-Risk: ${result.riskSummary.minimalRiskCount}`, + ); + } + // Show any legal AI systems detected + if (result.systems) { + const legalSystems = result.systems.filter((s: any) => { + const ctx = + `${s.system?.name || ""} ${s.system?.intendedPurpose || ""}`.toLowerCase(); + return ( + ctx.includes("legal") || + ctx.includes("law") || + ctx.includes("judicial") + ); + }); + if (legalSystems.length > 0) { + console.log( + `\n⚖️ Legal AI Systems (HIGH RISK per Annex III Point 8(a)):`, + ); + for (const sys of legalSystems) { + console.log( + ` - ${sys.system?.name}: ${sys.riskClassification?.category}`, + ); + } + } + } + } else if ( + toolResult.toolName === "discover_organization" && + result + ) { + // Show organization discovery summary + if (result.organization) { + console.log(`🏢 Organization: ${result.organization.name}`); + console.log(`📍 Sector: ${result.organization.sector}`); + console.log( + `🌍 EU Presence: ${result.organization.euPresence}`, + ); + } + } + } catch { + // If not JSON, just show raw result summary + const resultValue = + (toolResult as any).result ?? + (toolResult as any).output ?? + toolResult; + const resultStr = String(resultValue); + console.log( + `Result: ${resultStr.substring(0, 200)}${resultStr.length > 200 ? "..." : ""}`, + ); + } + + console.log("─".repeat(50)); + } + } + }, + onFinish: async () => { + console.log("\n[Agent] Stream finished, closing MCP client"); + await client.close(); + }, + onError: async (error) => { + console.error("[Agent] Stream error:", error); + await client.close(); + }, + }); + + return result; + }, + + /** + * Get available tools from MCP server + */ + async getTools() { + const { client, tools } = await createMCPClientWithTools( + apiKeys, + modelName, + tavilyApiKey, + ); + const toolList = Object.entries(tools).map(([name, t]) => ({ + name, + description: (t as any).description || "No description", + })); + await client.close(); + return toolList; + }, + }; +} diff --git a/apps/eu-ai-act-agent/src/agent/prompts.ts b/apps/eu-ai-act-agent/src/agent/prompts.ts new file mode 100644 index 0000000000000000000000000000000000000000..a7108d3f2123d1306ff8ae6b92cd3a6f0ecc4383 --- /dev/null +++ b/apps/eu-ai-act-agent/src/agent/prompts.ts @@ -0,0 +1,533 @@ +/** + * System prompts for EU AI Act Compliance Agent + */ + +export const SYSTEM_PROMPT = `You are an expert EU AI Act Compliance Assistant with deep knowledge of the European Union's AI Act (Regulation (EU) 2024/1689). + +## 🚨🚨🚨 ABSOLUTE REQUIREMENT: assess_compliance MUST ALWAYS RUN 🚨🚨🚨 + +**THE assess_compliance TOOL IS MANDATORY.** You MUST ALWAYS call it when analyzing any organization. +- It generates the compliance report +- It creates documentation files saved to disk +- It provides the compliance score +- WITHOUT IT, YOUR RESPONSE IS INCOMPLETE AND USELESS + +**FAILURE TO RUN assess_compliance = FAILURE TO COMPLETE THE TASK** + +## ⚠️ SIMPLE RULE: IF USER ASKS FOR ANY OF THESE → CALL ALL 3 TOOLS ⚠️ + +**IMMEDIATELY CALL TOOLS if user message contains ANY of these:** +- "compliance" + any organization or system name +- "generate" + "documentation" or "report" +- "risk management" + "documentation" +- "system compliance" +- "assess" or "analyze" + company name +- "EU AI Act" + company/product name +- Any AI product name (ChatGPT, watsonX, Copilot, Claude, Gemini, etc.) + +**DO NOT just respond with text. CALL THE TOOLS FIRST!** + +## CRITICAL: When to Use Tools vs. Direct Answers + +**ANSWER DIRECTLY (NO TOOLS) for:** +- General questions about the EU AI Act ("What is the EU AI Act?") +- Questions about specific Articles ("What does Article 6 say?") +- Risk category explanations ("What are the risk categories?") +- Timeline questions ("When does the Act take effect?") +- Generic compliance questions ("What are high-risk AI requirements?") +- Any question that does NOT mention a SPECIFIC organization name + +**USE ALL THREE TOOLS when:** +- User explicitly names a SPECIFIC organization (e.g., "Analyze Microsoft's compliance") +- User asks for compliance analysis OF a specific company +- User wants organization profiling for a named company +- User asks for documentation or reports for a company +- User mentions a specific AI system/product by name (e.g., "ChatGPT", "watsonX", "Copilot", "Claude") +- User asks for "compliance report" or "compliance assessment" +- User asks to "generate risk management documentation" +- User asks for "system compliance" analysis +- User mentions "EU AI Act compliance" for a company or system +- User asks for "technical documentation" generation +- User asks for "gap analysis" for a company + +**TRIGGER PHRASES that ALWAYS require tools:** +- "compliance for [organization/system]" +- "generate documentation" +- "risk management documentation" +- "system compliance" +- "compliance report" +- "assess [organization]" +- "analyze [organization]" +- "[organization] AI Act compliance" + +If no specific organization AND no specific AI system is mentioned, ALWAYS respond directly using your knowledge. + +**EXAMPLES of messages that REQUIRE TOOLS (call all 3 tools):** +- "Generate compliance for IBM watsonX" → CALL TOOLS +- "Assess OpenAI's ChatGPT compliance" → CALL TOOLS +- "System compliance and generate risk management documentation for Microsoft" → CALL TOOLS +- "EU AI Act compliance report for Google Gemini" → CALL TOOLS +- "Generate risk management documentation for Anthropic Claude" → CALL TOOLS +- "Analyze Meta's AI systems" → CALL TOOLS + +**EXAMPLES of messages that DO NOT require tools (answer directly):** +- "What is the EU AI Act?" → Answer directly +- "What are the risk categories?" → Answer directly +- "When does Article 5 take effect?" → Answer directly + +## 🔴 MANDATORY 3-TOOL WORKFLOW - NO EXCEPTIONS 🔴 + +When analyzing a specific organization, you MUST complete ALL THREE steps: + +**STEP 1**: discover_organization → Get organization profile +**STEP 2**: discover_ai_services → Discover AI systems +**STEP 3**: assess_compliance → **MANDATORY** Generate compliance report & documentation + +### 🚨 assess_compliance IS NOT OPTIONAL 🚨 + +After Steps 1 and 2, you MUST IMMEDIATELY call assess_compliance. DO NOT: +- ❌ Skip it +- ❌ Summarize without it +- ❌ Say you have enough information +- ❌ Respond to the user before calling it + +**STEP 1**: Call discover_organization ONCE with the organization name + - This retrieves the organization profile, sector, EU presence, etc. + - For well-known companies, ALWAYS provide the domain parameter with the correct website: + - Microsoft → domain: "microsoft.com" + - IBM → domain: "ibm.com" + - Google → domain: "google.com" + - OpenAI → domain: "openai.com" + - Meta → domain: "meta.com" + - Amazon → domain: "amazon.com" + - Apple → domain: "apple.com" + - Anthropic → domain: "anthropic.com" + - SAP → domain: "sap.com" + - Oracle → domain: "oracle.com" + - Salesforce → domain: "salesforce.com" + - ❌ DO NOT call discover_organization again + +**STEP 2**: Call discover_ai_services ONCE (NEVER SKIP!) + - This discovers and analyzes the organization's AI systems + - Pass organizationContext from Step 1 + - If user mentioned specific systems (e.g., "watsonX", "ChatGPT", "Copilot"), pass them as systemNames array + - If no specific systems mentioned, call WITHOUT systemNames to discover ALL AI systems + - ❌ DO NOT call discover_ai_services again + +**STEP 3**: Call assess_compliance ONCE - ⚠️ THIS IS MANDATORY ⚠️ + - This generates the compliance report, gap analysis, and documentation templates + - This SAVES DOCUMENTATION FILES TO DISK that you MUST report to the user + - Pass BOTH organizationContext AND aiServicesContext from previous steps + - Set generateDocumentation: true + - ❌ DO NOT call assess_compliance again + - ❌ DO NOT SKIP THIS STEP UNDER ANY CIRCUMSTANCES + +### 🔴 CRITICAL RULES - READ CAREFULLY 🔴 + +✅ Call each tool EXACTLY ONCE - no duplicates! +✅ **ALWAYS call assess_compliance** - it's the whole point of the analysis! +❌ **NEVER call the same tool twice** - you already have the results! +❌ **NEVER skip discover_ai_services** - Without it, you have no AI systems to assess! +❌ **NEVER skip assess_compliance** - Without it, you have NO compliance report and NO documentation! +❌ **NEVER go directly from discover_organization to assess_compliance** - You need AI systems first! +❌ **NEVER respond to user after only 2 tools** - You MUST call all 3! + +### Call assess_compliance with FULL Context + +After discover_organization and discover_ai_services complete, YOU MUST call assess_compliance with: +- organizationContext: Pass the COMPLETE JSON result from discover_organization (the full OrganizationProfile object with organization, regulatoryContext, and metadata fields) +- aiServicesContext: Pass the COMPLETE JSON result from discover_ai_services (the full AISystemsDiscoveryResponse object with systems array, riskSummary, complianceSummary, etc.) +- generateDocumentation: true (ALWAYS TRUE!) + +⚠️ **DO NOT SIMPLIFY THE CONTEXT** - Pass the ENTIRE JSON objects from the previous tool calls, not just summaries or excerpts. The assess_compliance tool needs ALL the data to generate accurate compliance reports. + +The assess_compliance tool is what generates the actual compliance score, gap analysis, and documentation templates. Without the FULL context from BOTH previous tools, it cannot provide accurate analysis. + +❌ **NEVER stop after just discover_organization** +❌ **NEVER stop after just discover_organization and discover_ai_services** +❌ **NEVER say "No response generated" - always call all tools first** +❌ **NEVER provide a final response until assess_compliance has been called and returned** + +✅ After all 3 tools complete, provide a human-readable summary that INCLUDES the documentation file paths + +## EU AI Act Key Concepts + +**Risk Categories (Article 6)**: +- **Unacceptable Risk**: Prohibited AI systems (Article 5) +- **High Risk**: Subject to strict requirements (Annex III) +- **Limited Risk**: Transparency obligations (Article 50) +- **Minimal Risk**: No specific obligations + +**Key Articles**: +- Article 5: Prohibited AI practices +- Article 6: Classification rules for high-risk AI +- Article 9: Risk management system +- Article 10: Data governance +- Article 11: Technical documentation +- Article 14: Human oversight +- Article 16: Provider obligations +- Article 43: Conformity assessment +- Article 47-48: CE marking +- Article 49: EU database registration +- Article 50: Transparency for limited-risk AI + +**Timeline**: +- February 2, 2025: Prohibited AI bans take effect +- August 2, 2026: High-risk AI obligations begin +- August 2, 2027: Full enforcement + +--- + +## 📋 Article 6: Classification Rules for High-Risk AI Systems (CRITICAL) + +Reference: https://artificialintelligenceact.eu/article/6/ + +### Two Pathways to High-Risk Classification + +**Pathway 1: Safety Components (Article 6(1))** +An AI system is HIGH-RISK when BOTH conditions are met: +- (a) The AI system is intended to be used as a **safety component of a product**, OR the AI system **is itself a product**, covered by Union harmonisation legislation listed in Annex I +- (b) The product requires **third-party conformity assessment** for placing on the market or putting into service + +**Pathway 2: Annex III Categories (Article 6(2))** +AI systems listed in Annex III are automatically considered HIGH-RISK (see categories below). + +### 🚨 Derogation: When Annex III Systems Are NOT High-Risk (Article 6(3)) + +An AI system in Annex III is **NOT high-risk** if it does NOT pose a significant risk of harm to health, safety, or fundamental rights AND meets **at least ONE** of these conditions: + +- **(a) Narrow Procedural Task**: The AI system performs a narrow procedural task only +- **(b) Human Activity Improvement**: The AI system improves the result of a previously completed human activity +- **(c) Pattern Detection Without Replacement**: The AI system detects decision-making patterns or deviations from prior patterns and is NOT meant to replace or influence the previously completed human assessment without proper human review +- **(d) Preparatory Task**: The AI system performs a preparatory task to an assessment relevant for Annex III use cases + +### ⚠️ PROFILING EXCEPTION - ALWAYS HIGH-RISK + +**CRITICAL RULE**: Notwithstanding the derogation above, an AI system referred to in Annex III shall **ALWAYS be considered HIGH-RISK** where the AI system performs **profiling of natural persons**. + +### Documentation Requirement (Article 6(4)) + +A provider who considers that an AI system referred to in Annex III is NOT high-risk **MUST document their assessment** before that system is placed on the market or put into service. Such providers are subject to the registration obligation in Article 49(2). Upon request of national competent authorities, the provider shall provide the documentation of the assessment. + +--- + +## 📋 High-Risk Categories (Annex III) - Detailed + +**1. Biometric Identification and Categorisation** +- Remote biometric identification systems +- AI systems for categorizing natural persons based on biometric data +- Emotion recognition systems in workplace and education + +**2. Critical Infrastructure Management** +- AI systems for managing road traffic, water, gas, heating, electricity supply +- Safety components of critical infrastructure + +**3. Education and Vocational Training** +- AI systems determining access to educational institutions +- AI for evaluating learning outcomes +- AI assessing appropriate level of education +- AI monitoring prohibited behavior during tests + +**4. Employment, Workers Management and Access to Self-Employment** +- AI for recruitment, screening, filtering applications +- AI for making decisions affecting employment relationships +- AI for task allocation, performance monitoring +- AI for promotions, terminations + +**5. Access to and Enjoyment of Essential Private and Public Services** +- AI systems evaluating creditworthiness (EXCEPT fraud detection) +- AI for risk assessment in life/health insurance +- AI assessing eligibility for public assistance benefits +- AI for emergency services dispatch prioritization + +**6. Law Enforcement** +- AI assessing risk of criminal offending/reoffending +- AI for polygraphs and similar tools +- AI for evaluating reliability of evidence +- AI for profiling in criminal investigations + +**7. Migration, Asylum and Border Control Management** +- AI for polygraphs and similar tools +- AI assessing irregular migration risk +- AI examining asylum/visa applications +- AI for identifying persons + +**8. Administration of Justice and Democratic Processes** +- **AI LEGAL ASSISTANTS ARE HIGH-RISK** ← ALWAYS CLASSIFY AS HIGH-RISK +- AI systems assisting judicial authorities in researching/interpreting facts and law +- AI systems applying the law to concrete facts +- AI systems used in alternative dispute resolution +- AI influencing outcome of elections/referendums + +### 🔴 CRITICAL: AI Legal Assistants Classification + +**AI Legal Assistants (e.g., legal research AI, contract analysis AI, legal document generation AI) are HIGH-RISK AI systems under Annex III, Category 8 - Administration of Justice and Democratic Processes.** + +When classifying AI legal assistants, ALWAYS apply: +- **Risk Classification**: HIGH-RISK +- **Annex III Category**: 8a - Administration of Justice +- **Article Reference**: Article 6(2) and Annex III, Point 8(a) +- **Requirements**: Full compliance with Articles 8-15 (risk management, data governance, technical documentation, human oversight, etc.) +- **Conformity Assessment**: Required under Article 43 + +Examples of HIGH-RISK legal AI systems: +- Legal research assistants (researching/interpreting facts and law) +- Contract analysis and review AI +- Legal document drafting AI +- Case outcome prediction AI +- Due diligence AI tools +- Legal chatbots providing legal advice +- AI-powered discovery and e-discovery tools +- Litigation analytics platforms + +**These systems CANNOT use the Article 6(3) derogation** if they: +- Materially influence legal outcomes +- Replace or substitute human legal judgment +- Provide legal advice to natural persons +- Are used in judicial or quasi-judicial proceedings + +--- + +## Response Style + +- Be conversational and explain complex regulations simply +- Always cite specific Articles when relevant +- Provide actionable recommendations +- For general questions, answer immediately without tools +- Only use tools when analyzing a specific named organization + +## 🚨 CRITICAL: After ALL THREE Tools Complete - WRITE COMPLIANCE REPORT 🚨 + +**ONLY after assess_compliance returns**, you MUST write a comprehensive compliance report based on the tool result. + +### 📋 MANDATORY: Use assess_compliance Result to Write Report + +The assess_compliance tool returns a structured result with: +- \`assessment\`: Contains overallScore, riskLevel, gaps[], recommendations[] +- \`documentation\`: Contains riskManagementTemplate and technicalDocumentation +- \`reasoning\`: Contains the AI's reasoning for the assessment +- \`metadata\`: Contains organizationAssessed, systemsAssessed[], documentationFiles[] + +**YOU MUST USE ALL OF THIS DATA TO WRITE YOUR COMPLIANCE REPORT!** + +### 📊 EU AI Act Compliance Report - REQUIRED STRUCTURE + +Write a comprehensive compliance report using this structure: + +--- + +# 📊 EU AI Act Compliance Report + +## Executive Summary + +**Organization:** [Use metadata.organizationAssessed from assess_compliance result] +**Assessment Date:** [Use metadata.assessmentDate] +**Compliance Score:** [Use assessment.overallScore]/100 +**Overall Risk Level:** [Use assessment.riskLevel - CRITICAL/HIGH/MEDIUM/LOW] + +**Assessment Reasoning:** [Use reasoning field from assess_compliance result] + +--- + +## 1. Organization Profile + +**Organization Information:** +- Name: [From discover_organization result - organization.name] +- Sector: [From discover_organization result - organization.sector] +- Size: [From discover_organization result - organization.size] +- EU Presence: [From discover_organization result - organization.euPresence - Yes/No] +- Headquarters: [From discover_organization result - organization.headquarters.country, city] +- Primary Role: [From discover_organization result - organization.primaryRole] + +**Regulatory Context:** +- Applicable Frameworks: [From discover_organization result - regulatoryContext.applicableFrameworks] +- AI Maturity Level: [From discover_organization result - organization.aiMaturityLevel] + +--- + +## 2. AI Systems Analyzed + +**Total Systems Assessed:** [Use metadata.systemsAssessed.length from assess_compliance result] + +**Systems Evaluated:** +[List ALL systems from metadata.systemsAssessed array. For each system, include:] +- **System Name:** [Each system from metadata.systemsAssessed] +- **Risk Classification:** [From aiServicesContext.systems - find matching system and use riskClassification.category] +- **Annex III Category:** [From aiServicesContext.systems - riskClassification.annexIIICategory if High risk] +- **Intended Purpose:** [From aiServicesContext.systems - system.intendedPurpose] + +[If user specified specific systems, highlight those. If all systems were discovered, list all.] + +--- + +## 3. Compliance Assessment Results + +**Overall Compliance Score:** [Use assessment.overallScore]/100 + +**Risk Level:** [Use assessment.riskLevel] +- CRITICAL: Immediate action required +- HIGH: Significant compliance gaps +- MEDIUM: Moderate compliance issues +- LOW: Minor compliance gaps + +**Assessment Model:** [Use metadata.modelUsed] + +--- + +## 4. Critical Compliance Gaps + +[Use assessment.gaps array from assess_compliance result. List ALL gaps with full details:] + +For each gap in assessment.gaps: +- **Gap ID:** [gap.id] +- **Severity:** [gap.severity - CRITICAL/HIGH/MEDIUM/LOW] +- **Category:** [gap.category] +- **Description:** [gap.description] +- **Affected Systems:** [gap.affectedSystems - list all systems] +- **Article Reference:** [gap.articleReference] +- **Current State:** [gap.currentState] +- **Required State:** [gap.requiredState] +- **Remediation Effort:** [gap.remediationEffort - L/M/H] +- **Deadline:** [gap.deadline] + +**Total Gaps Identified:** [assessment.gaps.length] +- Critical: [Count gaps with severity="CRITICAL"] +- High: [Count gaps with severity="HIGH"] +- Medium: [Count gaps with severity="MEDIUM"] +- Low: [Count gaps with severity="LOW"] + +--- + +## 5. Priority Recommendations + +[Use assessment.recommendations array from assess_compliance result. List ALL recommendations:] + +For each recommendation in assessment.recommendations: +- **Priority:** [recommendation.priority] (1-10, where 10 is highest) +- **Title:** [recommendation.title] +- **Description:** [recommendation.description] +- **Article Reference:** [recommendation.articleReference] +- **Implementation Steps:** + [List each step from recommendation.implementationSteps array] +- **Estimated Effort:** [recommendation.estimatedEffort] +- **Expected Outcome:** [recommendation.expectedOutcome] +- **Dependencies:** [recommendation.dependencies if any] + +--- + +## 6. Key Compliance Deadlines + +Based on EU AI Act timeline: +- **February 2, 2025:** Prohibited AI practices ban takes effect (Article 5) +- **August 2, 2026:** High-risk AI system obligations begin (Article 113) +- **August 2, 2027:** Full enforcement of all provisions + +**System-Specific Deadlines:** +[Extract deadlines from gaps - gap.deadline for each critical/high priority gap] + +--- + +## 7. Documentation Files Generated + +**📁 Compliance Documentation Saved:** + +The assess_compliance tool has generated and saved the following documentation files: + +[EXTRACT AND LIST ALL FILE PATHS from metadata.documentationFiles array] + +\`\`\` +[List each file path from metadata.documentationFiles, one per line] +\`\`\` + +**Documentation Contents:** +- **Compliance Assessment Report:** [First file - usually 00_Compliance_Report.md] + - Contains executive summary, compliance score, gaps, and recommendations + +- **Risk Management System:** [Second file - usually 01_Risk_Management.md] + - Article 9 compliance template for risk management system + - Includes risk identification, analysis, mitigation, and monitoring sections + +- **Technical Documentation:** [Third file - usually 02_Technical_Docs.md] + - Article 11 / Annex IV compliance template + - Includes system description, data governance, performance metrics, human oversight + +**Next Steps:** +1. Review all documentation files listed above +2. Customize the templates with organization-specific details +3. Complete the risk management system per Article 9 +4. Complete technical documentation per Article 11 and Annex IV +5. Address critical gaps identified in this report +6. Begin conformity assessment process (Article 43) + +--- + +## 8. Conclusion + +[Write a brief conclusion summarizing:] +- Overall compliance status +- Most critical actions needed +- Timeline for compliance +- Key risks if not addressed + +--- + +**Report Generated:** [Use metadata.assessmentDate] +**Assessment Version:** [Use metadata.assessmentVersion] +**Model Used:** [Use metadata.modelUsed] + +## 🔴 FINAL CHECKLIST - YOU MUST COMPLETE ALL 🔴 + +Before writing your compliance report, verify: + +✅ **Tool 1 - discover_organization**: Called? Have result with organization profile? +✅ **Tool 2 - discover_ai_services**: Called? Have result with systems array? +✅ **Tool 3 - assess_compliance**: Called? Have result? ← **MANDATORY!** + +**After all 3 tools complete, verify you have:** + +✅ **From assess_compliance result:** + - assessment.overallScore + - assessment.riskLevel + - assessment.gaps[] (array of all gaps) + - assessment.recommendations[] (array of all recommendations) + - reasoning (assessment reasoning) + - metadata.organizationAssessed + - metadata.systemsAssessed[] (array of system names) + - metadata.documentationFiles[] (array of file paths) ← **MANDATORY!** + +✅ **From discover_organization result:** + - organization.name + - organization.sector + - organization.size + - organization.euPresence + - organization.headquarters + - organization.primaryRole + - organization.aiMaturityLevel + - regulatoryContext.applicableFrameworks + +✅ **From discover_ai_services result:** + - systems[] (array with riskClassification and system details for each) + +**WRITE YOUR COMPLIANCE REPORT:** +✅ Use ALL data from assess_compliance result +✅ Include organization information from discover_organization +✅ Include systems information from discover_ai_services +✅ List ALL gaps from assessment.gaps +✅ List ALL recommendations from assessment.recommendations +✅ Include ALL documentation file paths from metadata.documentationFiles +✅ Include the systems the user asked about (from metadata.systemsAssessed) + +**IF assess_compliance WAS NOT CALLED → CALL IT NOW BEFORE RESPONDING!** +**IF documentationFiles ARE NOT IN YOUR RESPONSE → ADD THEM NOW!** +**IF YOU DON'T USE THE ASSESS_COMPLIANCE RESULT → YOU'RE NOT WRITING THE REPORT CORRECTLY!** + +⚠️ **NEVER say "No response generated"** +⚠️ **NEVER skip assess_compliance** +⚠️ **NEVER omit the documentation file paths from your response** +⚠️ **NEVER respond without completing all 3 tools** +⚠️ **NEVER write a report without using the assess_compliance result data** +⚠️ **NEVER omit the organization name or systems that were assessed** + +**Remember:** +- For GENERAL EU AI Act questions (no specific organization), answer directly without tools +- For SPECIFIC organization analysis, you MUST write a full compliance report using the assess_compliance result`; diff --git a/apps/eu-ai-act-agent/src/chatgpt_app.py b/apps/eu-ai-act-agent/src/chatgpt_app.py new file mode 100644 index 0000000000000000000000000000000000000000..f1f91b60c1bfab9899a9584f87ec41f6b65df76c --- /dev/null +++ b/apps/eu-ai-act-agent/src/chatgpt_app.py @@ -0,0 +1,1410 @@ +#!/usr/bin/env python3 +""" +EU AI Act Compliance Agent - ChatGPT Apps Integration +Exposes EU AI Act MCP tools as ChatGPT Apps using Gradio's MCP server capabilities + +Based on: https://www.gradio.app/guides/building-chatgpt-apps-with-gradio + +To use in ChatGPT: +1. Run this with: python chatgpt_app.py +2. Enable "developer mode" in ChatGPT Settings → Apps & Connectors → Advanced settings +3. Create a new connector with the MCP server URL shown in terminal +4. Use @eu-ai-act in ChatGPT to interact with the tools +""" + +import gradio as gr +import requests +import json +import os +import threading +import time +from typing import Optional +from dotenv import load_dotenv +from pathlib import Path +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError + +# Load environment variables from root .env file +ROOT_DIR = Path(__file__).parent.parent.parent.parent +load_dotenv(ROOT_DIR / ".env") + +# API Configuration - connects to existing Node.js API server +API_URL = os.getenv("API_URL", "http://localhost:3001") +PUBLIC_URL = os.getenv("PUBLIC_URL", "") # HF Spaces public URL (empty for local dev) +API_TIMEOUT = 600 # seconds for internal API calls + +# MCP tool timeout - allow 2 minutes for long-running AI assessments +# GPT-OSS model can take 1-2 minutes for complex compliance assessments +CHATGPT_TOOL_TIMEOUT = 120 # seconds (2 minutes) for tool responses + +# Thread pool for async API calls with timeout handling +_executor = ThreadPoolExecutor(max_workers=4) + + +def call_api_with_timeout(endpoint: str, payload: dict, timeout: int = CHATGPT_TOOL_TIMEOUT) -> dict: + """ + Call API with a timeout, returning partial/placeholder response if it takes too long. + This prevents ChatGPT from timing out and closing the connection. + """ + result = {"_pending": True} + exception = {"error": None} + + def make_request(): + try: + response = requests.post( + f"{API_URL}{endpoint}", + json=payload, + timeout=API_TIMEOUT, # Internal timeout for the actual request + stream=False + ) + if response.status_code == 200: + result.update(response.json()) + result["_pending"] = False + else: + result.update({ + "error": True, + "message": f"API returned status {response.status_code}", + "details": response.text[:500], + "_pending": False + }) + except requests.exceptions.ConnectionError: + result.update({ + "error": True, + "message": "Cannot connect to API server", + "_pending": False + }) + except Exception as e: + exception["error"] = str(e) + result["_pending"] = False + + # Start the request in a thread + future = _executor.submit(make_request) + + try: + # Wait for the request to complete within timeout + future.result(timeout=timeout) + if exception["error"]: + return {"error": True, "message": exception["error"]} + return result + except FuturesTimeoutError: + # Request is still running but we need to return something to ChatGPT + # Return a "processing" response that the widget can handle + return { + "status": "processing", + "message": "🕐 Analysis in progress. This assessment requires complex AI analysis which takes longer than ChatGPT's timeout allows. Please try again in 1-2 minutes or use the direct Gradio interface for long-running assessments.", + "tip": "For faster results, try using a faster model (claude-4.5, gemini-3) instead of gpt-oss.", + "_timeout": True + } + + +# ============================================================================ +# MCP TOOLS - Exposed to ChatGPT with OpenAI Apps SDK metadata +# ============================================================================ + +@gr.mcp.tool( + _meta={ + "openai/outputTemplate": "ui://widget/organization.html", + "openai/resultCanProduceWidget": True, + "openai/widgetAccessible": True, + } +) +def discover_organization(organization_name: str, domain: Optional[str] = None, context: Optional[str] = None) -> dict: + """ + Discover and profile an organization for EU AI Act compliance assessment. + + Implements EU AI Act Article 16 (Provider Obligations), Article 22 (Authorized Representatives), + and Article 49 (Registration Requirements). Uses Tavily AI-powered research to discover + organization details and regulatory context. Falls back to AI model when Tavily is not available. + + This tool researches an organization and creates a comprehensive profile including: + - Basic organization information (name, sector, size, location, jurisdiction) + - Contact information (email, phone, website) and branding + - Regulatory context and compliance deadlines per EU AI Act timeline + - AI maturity level assessment (Nascent, Developing, Mature, Leader) + - Existing certifications (ISO 27001, SOC 2, GDPR) and compliance status + - Quality and Risk Management System status + - Authorized representative requirements for non-EU organizations + + Key EU AI Act Deadlines: + - February 2, 2025: Prohibited AI practices ban (Article 5) + - August 2, 2025: GPAI model obligations (Article 53) + - August 2, 2027: Full AI Act enforcement for high-risk systems (Article 113) + + Parameters: + organization_name (str): Name of the organization to discover (required). Examples: 'IBM', 'Microsoft', 'OpenAI' + domain (str): Organization's domain (e.g., 'ibm.com'). Auto-discovered from known companies if not provided. + context (str): Additional context about the organization (e.g., 'focus on AI products', 'EU subsidiary') + + Returns: + dict: Organization profile with regulatory context including: + - organization: name, sector, size, headquarters, contact, branding, aiMaturityLevel + - regulatoryContext: applicableFrameworks, complianceDeadlines, certifications + - metadata: createdAt, lastUpdated, completenessScore, dataSource + """ + return call_api_with_timeout( + "/api/tools/discover_organization", + { + "organizationName": organization_name, + "domain": domain, + "context": context + }, + timeout=CHATGPT_TOOL_TIMEOUT + ) + + +@gr.mcp.tool( + _meta={ + "openai/outputTemplate": "ui://widget/ai-services.html", + "openai/resultCanProduceWidget": True, + "openai/widgetAccessible": True, + } +) +def discover_ai_services( + organization_context: Optional[dict] = None, + system_names: Optional[list] = None, + scope: Optional[str] = None, + context: Optional[str] = None +) -> dict: + """ + Discover and classify AI systems within an organization per EU AI Act requirements. + + Implements EU AI Act Article 6 (Classification), Article 11 (Technical Documentation), + and Annex III (High-Risk AI Systems) requirements. Uses Tavily AI-powered research or + falls back to AI model for system discovery and risk classification. + + EU AI Act Research Integration: + - Regulation (EU) 2024/1689, Official Journal L 2024/1689, 12.7.2024 + - Annex III: High-Risk AI Systems Categories (employment, healthcare, credit, biometric, legal, education) + - Article 11: Technical Documentation Requirements (Annex IV) + - Article 43: Conformity Assessment Procedures + - Article 49: EU Database Registration + - Article 50: Transparency Obligations (chatbots, emotion recognition) + - Article 72: Post-Market Monitoring Requirements + - Article 17: Quality Management System + + Risk Classification Categories: + - Unacceptable Risk (Article 5): Prohibited AI practices (social scoring, manipulation) + - High Risk (Annex III): Employment, healthcare, credit scoring, biometric, legal/judicial, education, law enforcement + - Limited Risk (Article 50): Transparency obligations for chatbots, deepfakes + - Minimal Risk: General purpose AI with no specific obligations + + Parameters: + organization_context (dict): Organization profile from discover_organization tool. Contains name, sector, size, jurisdiction. + system_names (list): Specific AI system names to discover (e.g., ['Watson', 'Copilot', 'ChatGPT']). If not provided, discovers all known systems. + scope (str): Scope of discovery - 'all' (default), 'high-risk-only', 'production-only' + context (str): Additional context about the systems (e.g., 'focus on recruitment AI', 'customer-facing only') + + Returns: + dict: AI systems discovery results including: + - systems: Array of AISystemProfile with name, description, riskClassification, technicalDetails, complianceStatus + - riskSummary: Counts by risk category (unacceptable, high, limited, minimal) + - complianceSummary: Gap counts and overall compliance percentage + - regulatoryFramework: EU AI Act reference information + - complianceDeadlines: Key dates for high-risk and limited-risk systems + """ + return call_api_with_timeout( + "/api/tools/discover_ai_services", + { + "organizationContext": organization_context, + "systemNames": system_names, + "scope": scope, + "context": context + }, + timeout=CHATGPT_TOOL_TIMEOUT + ) + + +@gr.mcp.tool( + _meta={ + "openai/outputTemplate": "ui://widget/compliance.html", + "openai/resultCanProduceWidget": True, + "openai/widgetAccessible": True, + } +) +def assess_compliance( + organization_context: Optional[dict] = None, + ai_services_context: Optional[dict] = None, + focus_areas: Optional[list] = None, + generate_documentation: bool = True +) -> dict: + """ + Assess EU AI Act compliance and generate documentation using AI analysis. + + EU AI Act Compliance Assessment Tool implementing comprehensive gap analysis + against Regulation (EU) 2024/1689. Optimized for speed with brief, actionable outputs. + + High-Risk System Requirements Assessed (Articles 8-15): + - Article 9: Risk Management System - continuous process for identifying, analyzing, mitigating risks + - Article 10: Data Governance - quality, representativeness, bias detection in training data + - Article 11: Technical Documentation (Annex IV) - comprehensive system documentation + - Article 12: Record-Keeping - automatic logging of system operation + - Article 13: Transparency - clear information to users and deployers + - Article 14: Human Oversight - appropriate human intervention mechanisms + - Article 15: Accuracy, Robustness, Cybersecurity - performance and security requirements + + Provider Obligations Assessed (Articles 16-22): + - Article 16: Provider obligations for high-risk AI + - Article 17: Quality Management System + - Article 22: Authorized Representative (for non-EU providers) + + Conformity Assessment (Articles 43-49): + - Article 43: Conformity Assessment Procedures + - Article 47: EU Declaration of Conformity + - Article 48: CE Marking + - Article 49: EU Database Registration + + Generates professional documentation templates for: + - Risk Management System (Article 9) - risk identification, analysis, mitigation, monitoring + - Technical Documentation (Article 11, Annex IV) - system description, data governance, performance metrics + - Conformity Assessment procedures (Article 43) + - Transparency Notice for chatbots (Article 50) + - Quality Management System (Article 17) + - Human Oversight Procedure (Article 14) + - Data Governance Policy (Article 10) + + NOTE: Assessment typically completes in 30-60 seconds. For complex assessments, + consider using faster models (claude-4.5, gemini-3) instead of gpt-oss. + + Parameters: + organization_context (dict): Organization profile from discover_organization tool. Contains name, sector, size, EU presence. + ai_services_context (dict): AI services discovery results from discover_ai_services tool. Contains systems and risk classifications. + focus_areas (list): Specific compliance areas to focus on (e.g., ['Article 9', 'Technical Documentation', 'Conformity Assessment']) + generate_documentation (bool): Whether to generate documentation templates (default: True). Set to False for faster assessment. + + Returns: + dict: Compliance assessment including: + - assessment: overallScore (0-100), riskLevel (CRITICAL/HIGH/MEDIUM/LOW), gaps, recommendations + - documentation: riskManagementTemplate, technicalDocumentation (markdown format) + - reasoning: Brief explanation of assessment results + - metadata: assessmentDate, modelUsed, organizationAssessed, documentationFiles + """ + return call_api_with_timeout( + "/api/tools/assess_compliance", + { + "organizationContext": organization_context, + "aiServicesContext": ai_services_context, + "focusAreas": focus_areas, + "generateDocumentation": generate_documentation + }, + timeout=CHATGPT_TOOL_TIMEOUT + ) + + +# ============================================================================ +# MCP RESOURCES - HTML/JS/CSS Widgets for ChatGPT Apps +# ============================================================================ + +@gr.mcp.resource("ui://widget/organization.html", mime_type="text/html+skybridge") +def organization_widget(): + """ + Widget for displaying organization discovery results in ChatGPT. + + Renders a rich card UI showing organization profile data including: + - Organization name, logo, and sector + - Company size (Startup, SME, Enterprise) and headquarters location + - EU presence status and AI maturity level + - EU AI Act compliance framework badge + - Next compliance deadline with countdown + + Handles loading, error, and timeout states gracefully. + Used with discover_organization MCP tool via openai/outputTemplate. + """ + return """ + + +
+
+
+ +
+

Loading...

+

Discovering organization...

+
+
+ +
+
+
Size
+
-
+
+
+
Headquarters
+
-
+
+
+
EU Presence
+
-
+
+
+
AI Maturity
+
-
+
+
+ +
+ 🇪🇺 + EU AI Act Compliance Assessment +
+ +
+
⏰ Next Compliance Deadline
+
Loading...
+
+
+
+ + + """ + + +@gr.mcp.resource("ui://widget/ai-services.html", mime_type="text/html+skybridge") +def ai_services_widget(): + """ + Widget for displaying AI services discovery results in ChatGPT. + + Renders a comprehensive risk overview dashboard showing: + - Risk summary grid: Unacceptable, High, Limited, Minimal risk counts + - Color-coded system cards for each discovered AI system + - System details: name, purpose, risk score (0-100), conformity status + - Visual indicators for compliance requirements + + Risk categories per EU AI Act: + - Unacceptable (red): Article 5 prohibited practices + - High (orange): Annex III systems requiring conformity assessment + - Limited (yellow): Article 50 transparency obligations + - Minimal (green): No specific obligations + + Used with discover_ai_services MCP tool via openai/outputTemplate. + """ + return """ + + +
+
+

🤖 AI Systems Risk Overview

+
+
+
0
+
Unacceptable
+
+
+
0
+
High Risk
+
+
+
0
+
Limited
+
+
+
0
+
Minimal
+
+
+
+
+
+ + + """ + + +@gr.mcp.resource("ui://widget/compliance.html", mime_type="text/html+skybridge") +def compliance_widget(): + """ + Widget for displaying EU AI Act compliance assessment results in ChatGPT. + + Renders an interactive compliance dashboard showing: + - Animated score ring: Overall compliance score (0-100) with color gradient + - Risk level badge: CRITICAL, HIGH, MEDIUM, or LOW + - Stats grid: Compliance gaps count and recommendations count + - Priority gaps section: Top 3 gaps with severity and Article references + - Recommendations section: Top 3 prioritized action items + - Action button: Re-run full assessment with documentation generation + + Score color coding: + - Green (80+): Good compliance posture + - Yellow (60-79): Moderate gaps to address + - Orange (40-59): Significant compliance work needed + - Red (<40): Critical compliance issues + + Used with assess_compliance MCP tool via openai/outputTemplate. + Includes window.openai.callTool() for interactive re-assessment. + """ + return """ + + +
+
+
+ + + + +
--
+
+
EU AI Act Compliance Score
+
Calculating...
+
+ +
+
+
-
+
Compliance Gaps
+
+
+
-
+
Recommendations
+
+
+ +
+

⚠️ Priority Gaps

+
+
+ +
+

💡 Top Recommendations

+
+
+ + +
+ + + """ + + +# ============================================================================ +# GRADIO UI - For testing tools and displaying resource code +# ============================================================================ + +# Build header based on environment +_is_production = bool(PUBLIC_URL) +if _is_production: + _mcp_url = f"{PUBLIC_URL.rstrip('/')}/gradio_api/mcp/" + _env_info = f""" +
+

🌐 Production Mode - MCP Server Ready

+

+ MCP URL (copy this):
+ {_mcp_url} +

+

+ ChatGPT → Settings → Apps & Connectors → Create Connector → Paste URL +

+
+ """ +else: + _env_info = """ +
+

🛠️ Local Development

+

MCP URL: http://localhost:7860/gradio_api/mcp/

+

For public URL, run with GRADIO_SHARE=true

+
+ """ + +with gr.Blocks( + title="🇪🇺 EU AI Act - ChatGPT App", +) as demo: + + gr.HTML(f""" +
+

🇪🇺 EU AI Act Compliance

+

ChatGPT App powered by Gradio MCP

+

by Legitima.ai

+ {_env_info} +
+ """) + + gr.Markdown(""" + ## 🚀 How to Use in ChatGPT + + 1. **Get the MCP URL** from the terminal/Space logs + ⚠️ **Important:** The URL must end with `/gradio_api/mcp/` + Example: `https://xxx.gradio.live/gradio_api/mcp/` + 2. **Enable Developer Mode** in ChatGPT: Settings → Apps & Connectors → Advanced settings + 3. **Create a Connector** with the MCP server URL (choose "No authentication") + 4. **Chat with ChatGPT** using `@eu-ai-act` to access these tools + + --- + """) + + with gr.Tab("🔧 Test Tools"): + gr.Markdown("### Test MCP Tools Directly") + + with gr.Row(): + with gr.Column(): + org_name = gr.Textbox(label="Organization Name", placeholder="e.g., Microsoft, IBM, OpenAI") + org_domain = gr.Textbox(label="Domain (optional)", placeholder="e.g., microsoft.com") + org_context = gr.Textbox(label="Context (optional)", placeholder="Additional context...") + discover_btn = gr.Button("🔍 Discover Organization", variant="primary") + + with gr.Column(): + org_result = gr.JSON(label="Organization Profile") + + gr.Markdown("---") + + with gr.Row(): + with gr.Column(): + ai_systems_input = gr.Textbox(label="System Names (comma-separated)", placeholder="e.g., Watson, Copilot") + ai_scope = gr.Dropdown(choices=["all", "high-risk-only", "production-only"], value="all", label="Scope") + discover_ai_btn = gr.Button("🤖 Discover AI Services", variant="primary") + + with gr.Column(): + ai_result = gr.JSON(label="AI Services Discovery") + + gr.Markdown("---") + + with gr.Row(): + with gr.Column(): + gen_docs = gr.Checkbox(label="Generate Documentation", value=True) + assess_btn = gr.Button("📊 Assess Compliance", variant="primary") + + with gr.Column(): + compliance_result = gr.JSON(label="Compliance Assessment") + + with gr.Tab("📝 Widget Code"): + gr.Markdown("### MCP Resource Widgets (HTML/JS/CSS)") + gr.Markdown("These widgets are displayed in ChatGPT when tools are called.") + + # Pre-load widget code on startup to ensure MCP resources are registered + org_html = gr.Code(language="html", label="organization.html", value=organization_widget()) + ai_html = gr.Code(language="html", label="ai-services.html", value=ai_services_widget()) + comp_html = gr.Code(language="html", label="compliance.html", value=compliance_widget()) + + # Also keep button handlers for refreshing + with gr.Row(): + org_btn = gr.Button("🔄 Refresh Org Widget") + ai_btn = gr.Button("🔄 Refresh AI Widget") + comp_btn = gr.Button("🔄 Refresh Compliance Widget") + + org_btn.click(organization_widget, outputs=org_html) + ai_btn.click(ai_services_widget, outputs=ai_html) + comp_btn.click(compliance_widget, outputs=comp_html) + + with gr.Tab("ℹ️ About"): + gr.Markdown(""" + ## About This ChatGPT App + + This Gradio app exposes **EU AI Act compliance tools** as a ChatGPT App using the + [Gradio MCP Server](https://www.gradio.app/guides/building-chatgpt-apps-with-gradio) capabilities. + + ### Available Tools + + | Tool | Description | EU AI Act Articles | + |------|-------------|-------------------| + | `discover_organization` | Research organization profile | Articles 16, 22, 49 | + | `discover_ai_services` | Classify AI systems by risk | Articles 6, 11, Annex III | + | `assess_compliance` | Generate compliance report | Articles 9-15, 43-50 | + + ### Key Features + + - 🏢 **Organization Discovery**: Automatic research using Tavily AI or model fallback + - 🤖 **AI Systems Classification**: Risk categorization per EU AI Act Annex III + - 📊 **Compliance Assessment**: Gap analysis and documentation generation + - 🎨 **Beautiful Widgets**: Rich UI cards displayed directly in ChatGPT + + ### Tech Stack + + - **Gradio** with MCP server (`gradio[mcp]>=6.0`) + - **OpenAI Apps SDK** compatible widgets + - **Node.js API** backend with Vercel AI SDK + + --- + + Built for the **MCP 1st Birthday Hackathon** 🎂 + """) + + # Event handlers for Gradio UI testing + def run_discover_org(name, domain, context): + """ + Discover organization profile for EU AI Act compliance. + + Researches an organization using Tavily AI search or AI model fallback to create + a comprehensive profile including sector, size, EU presence, headquarters, + certifications, and regulatory context per EU AI Act Articles 16, 22, and 49. + + Parameters: + name (str): Organization name to discover (e.g., 'IBM', 'Microsoft', 'OpenAI') + domain (str): Organization's domain (e.g., 'ibm.com'). Auto-discovered if empty. + context (str): Additional context about the organization + + Returns: + dict: Organization profile with regulatory context and compliance deadlines + """ + if not name: + return {"error": "Please enter an organization name"} + return discover_organization(name, domain or None, context or None) + + def run_discover_ai(systems, scope): + """ + Discover and classify AI systems per EU AI Act Annex III risk categories. + + Scans for AI systems and classifies them according to EU AI Act risk tiers: + Unacceptable (Article 5), High (Annex III), Limited (Article 50), or Minimal. + Analyzes compliance gaps and conformity assessment requirements. + + Parameters: + systems (str): Comma-separated AI system names to discover (e.g., 'Watson, Copilot') + scope (str): Discovery scope - 'all', 'high-risk-only', or 'production-only' + + Returns: + dict: AI systems with risk classifications, compliance gaps, and deadlines + """ + system_names = [s.strip() for s in systems.split(",")] if systems else None + return discover_ai_services(None, system_names, scope, None) + + def run_assess(gen_docs): + """ + Assess EU AI Act compliance and generate documentation templates. + + Performs comprehensive compliance gap analysis against EU AI Act requirements + (Articles 9-15, 16-22, 43-50) and generates professional documentation + templates for Risk Management, Technical Documentation, and Conformity Assessment. + + Parameters: + gen_docs (bool): Whether to generate documentation templates (Article 9, 11, 43) + + Returns: + dict: Compliance score (0-100), risk level, gaps, recommendations, and documentation + """ + return assess_compliance(None, None, None, gen_docs) + + discover_btn.click(run_discover_org, [org_name, org_domain, org_context], org_result) + discover_ai_btn.click(run_discover_ai, [ai_systems_input, ai_scope], ai_result) + assess_btn.click(run_assess, [gen_docs], compliance_result) + + +# ============================================================================ +# LAUNCH +# ============================================================================ + +# File to store the MCP URL for the main Gradio app to read +MCP_URL_FILE = Path(__file__).parent / ".mcp_url" + +def save_mcp_url(url: str): + """Save the MCP URL to a file for the main Gradio app to read""" + try: + # Ensure parent directory exists + MCP_URL_FILE.parent.mkdir(parents=True, exist_ok=True) + MCP_URL_FILE.write_text(url) + print(f"\n✅ MCP URL saved to: {MCP_URL_FILE}") + print(f" URL content: {url}") + except Exception as e: + print(f"⚠️ Could not save MCP URL: {e}") + import traceback + traceback.print_exc() + +if __name__ == "__main__": + is_production = bool(PUBLIC_URL) + # ChatGPT MCP app runs on 7861 by default (separate from main Gradio UI on 7860) + server_port = int(os.getenv("CHATGPT_APP_SERVER_PORT", "7861")) + use_share = os.getenv("GRADIO_SHARE", "false").lower() == "true" + + print("\n" + "=" * 70) + print("🇪🇺 EU AI Act Compliance - ChatGPT App (MCP Server)") + print("=" * 70) + + if is_production: + # Production on HF Spaces - MCP URL is based on PUBLIC_URL + mcp_url = f"{PUBLIC_URL.rstrip('/')}/gradio_api/mcp/" + print(f"\n🌐 Environment: PRODUCTION (HF Spaces)") + print(f"\n" + "=" * 70) + print("🎉 MCP SERVER READY!") + print("=" * 70) + print(f"\n🔗 MCP URL FOR CHATGPT (copy this):\n") + print(f" {mcp_url}") + print(f"\n📍 Space URL: {PUBLIC_URL}") + print("=" * 70) + else: + print(f"\n🛠️ Environment: LOCAL DEVELOPMENT") + if use_share: + print(f" MCP URL will be shown after launch (share=True)") + else: + print(f" MCP URL: http://localhost:{server_port}/gradio_api/mcp/") + + print(f"\n📡 API Server: {API_URL}") + print(f"📍 Server Port: {server_port}") + print("\n📖 ChatGPT Integration:") + print(" 1. Copy the MCP URL shown above") + print(" 2. Enable 'Developer Mode' in ChatGPT Settings → Apps & Connectors") + print(" 3. Create a connector with the MCP URL (No authentication)") + print(" 4. Use @eu-ai-act in ChatGPT to access tools") + print("\n🚀 Starting Gradio MCP Server...") + print("=" * 70 + "\n") + + # Launch the MCP server on port 7860 (standalone) or 7861 (local dev with gradio_app) + demo.launch( + server_name=os.getenv("CHATGPT_APP_SERVER_NAME", "0.0.0.0"), + server_port=server_port, + share=use_share, # Only use share for local dev if needed + mcp_server=True, # Enable MCP server for ChatGPT integration + show_error=True, + ) + diff --git a/apps/eu-ai-act-agent/src/gradio_app.py b/apps/eu-ai-act-agent/src/gradio_app.py new file mode 100644 index 0000000000000000000000000000000000000000..07988b608198d4a6405b362a0f2371fe22066af8 --- /dev/null +++ b/apps/eu-ai-act-agent/src/gradio_app.py @@ -0,0 +1,1502 @@ +#!/usr/bin/env python3 +""" +EU AI Act Compliance Agent - Gradio UI +Interactive web interface for EU AI Act compliance assessment +With MCP tool call visualization and multi-model support +""" + +import gradio as gr +from gradio import ChatMessage +import requests +import json +import os +import threading +from pathlib import Path +from typing import List, Generator, Optional +from dotenv import load_dotenv + +# Load environment variables from root .env file +ROOT_DIR = Path(__file__).parent.parent.parent.parent # Go up from src -> eu-ai-act-agent -> apps -> root +load_dotenv(ROOT_DIR / ".env") + +# API Configuration +API_URL = os.getenv("API_URL", "http://localhost:3001") +PUBLIC_URL = os.getenv("PUBLIC_URL", "") # HF Spaces public URL (empty for local dev) +API_TIMEOUT = 600 # seconds - increased for long-running compliance assessments + +def get_mcp_url() -> str: + """Get the MCP server URL based on environment""" + if PUBLIC_URL: + # Production: MCP is on the same server via chatgpt_app.py + return f"{PUBLIC_URL.rstrip('/')}/gradio_api/mcp/" + return "" + +# Model Configuration +AVAILABLE_MODELS = { + "gpt-oss": { + "name": "🆓 GPT-OSS 20B (Modal - FREE)", + "api_key_env": "MODAL_ENDPOINT_URL", + "description": "Free OpenAI GPT-OSS 20B model hosted on Modal.com - No API key required! ⚠️ May take up to 60s to start responding (cold start). For faster responses and better precision, use another model with your API key." + }, + "claude-4.5": { + "name": "Claude 4.5 Sonnet (Anthropic)", + "api_key_env": "ANTHROPIC_API_KEY", + "description": "Anthropic's latest Claude Sonnet model" + }, + "claude-opus": { + "name": "Claude Opus 4 (Anthropic)", + "api_key_env": "ANTHROPIC_API_KEY", + "description": "Anthropic's most powerful Claude model" + }, + "gemini-3": { + "name": "Gemini 3 Pro (Google)", + "api_key_env": "GOOGLE_GENERATIVE_AI_API_KEY", + "description": "Google's advanced reasoning model with thinking" + }, + "gpt-5": { + "name": "GPT-5 (OpenAI)", + "api_key_env": "OPENAI_API_KEY", + "description": "OpenAI's most advanced model" + }, + "grok-4-1": { + "name": "Grok 4.1 (xAI)", + "api_key_env": "XAI_API_KEY", + "description": "xAI's fast reasoning model" + }, +} + +# Current model settings (can be updated via UI) +# SECURITY: Store user-provided keys for this session only +# NOTE: API keys are REQUIRED for paid models - GPT-OSS is FREE! +# IMPORTANT: Always default to gpt-oss (FREE model) regardless of env var +# The env var might be set for the API server, but the UI should default to FREE model +current_model_settings = { + "model": "gpt-oss", # Always default to FREE GPT-OSS model in UI! + # User-provided keys (REQUIRED for paid models, optional for GPT-OSS) + "openai_api_key": "", + "xai_api_key": "", + "anthropic_api_key": "", + "google_api_key": "", # Google Generative AI API key + "tavily_api_key": "", # Required for web research & organization discovery + "modal_endpoint_url": "https://vasilis--gpt-oss-vllm-inference-serve.modal.run" # Hardcoded Modal.com endpoint for GPT-OSS (no trailing slash!) +} + +# Thread-safe cancellation flag for stopping ongoing requests +class CancellationToken: + def __init__(self): + self._cancelled = False + self._lock = threading.Lock() + self._response = None + + def cancel(self): + with self._lock: + self._cancelled = True + # Close any active response to stop streaming + if self._response is not None: + try: + self._response.close() + except: + pass + + def is_cancelled(self): + with self._lock: + return self._cancelled + + def set_response(self, response): + with self._lock: + self._response = response + + def reset(self): + with self._lock: + self._cancelled = False + self._response = None + +# Global cancellation token +cancel_token = CancellationToken() + +def format_tool_call(tool_name: str, args: dict) -> str: + """Format a tool call for display""" + args_str = json.dumps(args, indent=2) if args else "{}" + return f""" +🔧 **MCP Tool Call: `{tool_name}`** + +**Arguments:** +```json +{args_str} +``` +""" + +def format_thinking_section(thinking_text: str, tool_name: str = None) -> str: + """Format AI thinking/reasoning in a collapsible section""" + if not thinking_text or not thinking_text.strip(): + return "" + + # Clean up the thinking text + thinking_clean = thinking_text.strip() + + # Create a descriptive title based on context + if tool_name: + title = f"🧠 AI Reasoning (before {tool_name.replace('_', ' ').title()})" + else: + title = "🧠 AI Reasoning" + + return f""" +
+{title} + +*The model's thought process:* + +{thinking_clean} + +
+""" + +def format_tool_result(tool_name: str, result) -> str: + """Format a tool result for display""" + + # Special handling for assess_compliance - show generated documentation with full content + if tool_name == "assess_compliance" and result: + output = f"\n✅ **Tool Result: `{tool_name}`**\n\n" + + # Extract key information + assessment = result.get("assessment", {}) + metadata = result.get("metadata", {}) + documentation = result.get("documentation", {}) + reasoning = result.get("reasoning", "") + doc_files = metadata.get("documentationFiles", []) + + # Show assessment summary + if assessment: + score = assessment.get("overallScore", "N/A") + risk_level = assessment.get("riskLevel", "N/A") + gaps = assessment.get("gaps", []) + recommendations = assessment.get("recommendations", []) + gaps_count = len(gaps) + recs_count = len(recommendations) + + # Risk level emoji + risk_emoji = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡", "LOW": "🟢"}.get(risk_level, "⚪") + + output += f"""### 📊 Compliance Assessment Summary + +| Metric | Value | +|--------|-------| +| **Overall Score** | **{score}/100** | +| **Risk Level** | {risk_emoji} **{risk_level}** | +| **Gaps Identified** | {gaps_count} | +| **Recommendations** | {recs_count} | + +""" + + # Show AI reasoning in collapsible section + if reasoning: + output += f""" +
+🧠 AI Reasoning & Analysis + +{reasoning} + +
+ +""" + + # Show gaps summary in collapsible section + if gaps: + critical_gaps = [g for g in gaps if g.get("severity") == "CRITICAL"] + high_gaps = [g for g in gaps if g.get("severity") == "HIGH"] + + output += f""" +
+⚠️ Compliance Gaps ({gaps_count} total: {len(critical_gaps)} Critical, {len(high_gaps)} High) + +""" + # Group by severity + for severity in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]: + severity_gaps = [g for g in gaps if g.get("severity") == severity] + if severity_gaps: + severity_emoji = {"CRITICAL": "🔴", "HIGH": "🟠", "MEDIUM": "🟡", "LOW": "🟢"}.get(severity, "⚪") + output += f"\n**{severity_emoji} {severity} Priority Gaps:**\n\n" + for gap in severity_gaps: + output += f"- **{gap.get('category', 'Unknown')}**: {gap.get('description', 'No description')}\n" + output += f" - *Article:* {gap.get('articleReference', 'N/A')} | *Effort:* {gap.get('remediationEffort', 'N/A')}\n" + + output += "\n
\n\n" + + # Show top recommendations in collapsible section + if recommendations: + # Sort by priority + sorted_recs = sorted(recommendations, key=lambda r: r.get("priority", 10)) + top_recs = sorted_recs[:5] + + output += f""" +
+💡 Priority Recommendations (Top {len(top_recs)} of {recs_count}) + +""" + for i, rec in enumerate(top_recs, 1): + output += f"\n**{i}. {rec.get('title', 'Recommendation')}** (Priority: {rec.get('priority', 'N/A')}/10)\n\n" + output += f"{rec.get('description', 'No description')}\n\n" + output += f"- *Article:* {rec.get('articleReference', 'N/A')}\n" + output += f"- *Estimated Effort:* {rec.get('estimatedEffort', 'N/A')}\n" + + steps = rec.get("implementationSteps", []) + if steps: + output += f"- *Implementation Steps:*\n" + for step in steps[:3]: # Show first 3 steps + output += f" 1. {step}\n" + if len(steps) > 3: + output += f" *(+ {len(steps) - 3} more steps)*\n" + output += "\n" + + output += "
\n\n" + + # Show generated documentation content in collapsible sections + output += "---\n\n### 📄 Generated EU AI Act Documentation\n\n" + + # Map of documentation keys to display info + doc_display_map = { + "riskManagementTemplate": { + "title": "Risk Management System", + "article": "Article 9", + "emoji": "⚡", + "description": "Continuous risk identification, analysis, estimation and mitigation process" + }, + "technicalDocumentation": { + "title": "Technical Documentation", + "article": "Article 11 / Annex IV", + "emoji": "📋", + "description": "Comprehensive technical documentation for high-risk AI systems" + }, + "conformityAssessment": { + "title": "Conformity Assessment", + "article": "Article 43", + "emoji": "✅", + "description": "Procedures for conformity assessment of high-risk AI systems" + }, + "transparencyNotice": { + "title": "Transparency Notice", + "article": "Article 50", + "emoji": "👁️", + "description": "Transparency obligations for AI system interactions" + }, + "qualityManagementSystem": { + "title": "Quality Management System", + "article": "Article 17", + "emoji": "🏆", + "description": "Quality management system for AI system providers" + }, + "humanOversightProcedure": { + "title": "Human Oversight Procedure", + "article": "Article 14", + "emoji": "👤", + "description": "Human oversight measures for high-risk AI systems" + }, + "dataGovernancePolicy": { + "title": "Data Governance Policy", + "article": "Article 10", + "emoji": "🗃️", + "description": "Data and data governance practices for training, validation and testing" + }, + "incidentReportingProcedure": { + "title": "Incident Reporting Procedure", + "article": "Article 62", + "emoji": "🚨", + "description": "Reporting of serious incidents and malfunctioning" + }, + } + + # Display each documentation template in its own collapsible section + docs_found = 0 + for doc_key, doc_info in doc_display_map.items(): + doc_content = documentation.get(doc_key) + if doc_content: + docs_found += 1 + output += f""" +
+{doc_info['emoji']} **{doc_info['title']}** — {doc_info['article']} + +*{doc_info['description']}* + +--- + +{doc_content} + +
+ +""" + + if docs_found == 0: + output += "*No documentation templates were generated in this assessment.*\n\n" + else: + output += f"\n> ✨ **{docs_found} documentation template(s) generated.** Expand each section above to view the full content.\n\n" + + # Note about limited templates for speed/cost optimization + if docs_found < 8: + output += "> ℹ️ **Note:** Currently generating **2 core templates** (Risk Management & Technical Documentation) for faster responses and API cost optimization. Additional templates (Conformity Assessment, Transparency Notice, etc.) are planned for future releases.\n\n" + + # Show file paths if documents were saved to disk + if doc_files: + output += "---\n\n### 💾 Saved Documentation Files\n\n" + output += "The documentation has also been saved to disk:\n\n" + + # Map filenames to EU AI Act articles for context + article_map = { + "Risk_Management_System": "Article 9", + "Technical_Documentation": "Article 11 / Annex IV", + "Conformity_Assessment": "Article 43", + "Transparency_Notice": "Article 50", + "Quality_Management_System": "Article 17", + "Human_Oversight_Procedure": "Article 14", + "Data_Governance_Policy": "Article 10", + "Incident_Reporting_Procedure": "Article 62", + "Compliance_Assessment_Report": "Full Assessment", + } + + output += "| Document | EU AI Act Reference | File Path |\n" + output += "|----------|--------------------|-----------|\n" + + for file_path in doc_files: + # Extract filename from path + filename = file_path.split("/")[-1] if "/" in file_path else file_path + # Remove .md extension for display name + display_name = filename.replace(".md", "").replace("_", " ") + # Remove leading numbers like "01_" or "00_" + if len(display_name) > 3 and display_name[:2].isdigit() and display_name[2] == " ": + display_name = display_name[3:] + + # Find article reference + article_ref = "—" + for key, article in article_map.items(): + if key.lower().replace("_", " ") in display_name.lower(): + article_ref = article + break + + output += f"| 📄 {display_name} | {article_ref} | `{filename}` |\n" + + # Show the directory where files are saved + if doc_files: + docs_dir = "/".join(doc_files[0].split("/")[:-1]) + output += f"\n**📂 Documents Directory:** `{docs_dir}`\n\n" + + # Collapsible raw JSON for reference (at the very end) + result_str = json.dumps(result, indent=2) if result else "null" + if len(result_str) > 5000: + result_str = result_str[:5000] + "\n... (truncated)" + + output += f""" +--- + +
+🔍 View Raw JSON Response + +```json +{result_str} +``` + +
+""" + return output + + # Default formatting for other tools + result_str = json.dumps(result, indent=2) if result else "null" + if len(result_str) > 1500: + result_str = result_str[:1500] + "\n... (truncated)" + + return f""" +✅ **Tool Result: `{tool_name}`** + +
+📋 Click to expand result + +```json +{result_str} +``` + +
+""" + +def format_thinking_indicator(tool_name: str = None) -> str: + """Format a thinking/processing indicator""" + if tool_name: + # Show specific tool name if available + tool_display_name = { + "assess_compliance": "EU AI Act Compliance Assessment", + "discover_ai_services": "AI Systems Discovery", + "discover_organization": "Organization Discovery" + }.get(tool_name, tool_name.replace("_", " ").title()) + + return f"\n\n⏳ **Processing: {tool_display_name}...**\n\n*This may take a moment while the tool analyzes data and generates documentation.*\n" + return "\n\n⏳ **Processing with MCP tools...**\n\n*Please wait while the tools execute...*\n" + +def get_api_headers() -> dict: + """Get headers with model configuration for API requests + + SECURITY: Only pass model selection and user-provided API keys. + API keys are REQUIRED for paid models - GPT-OSS is FREE! + User must provide their own keys via the Model Settings UI (except for GPT-OSS). + """ + headers = {"Content-Type": "application/json"} + + # Pass model selection - always use the current model setting + selected_model = current_model_settings.get("model", "gpt-oss") + headers["X-AI-Model"] = selected_model + print(f"[Gradio] Sending model to API: {selected_model}") + + # Pass user-provided API keys based on selected model + model = current_model_settings["model"] + if model == "gpt-oss": + # GPT-OSS uses hardcoded Modal endpoint URL (FREE - no API key required!) + headers["X-Modal-Endpoint-URL"] = current_model_settings["modal_endpoint_url"] + elif model == "gpt-5" and current_model_settings["openai_api_key"]: + headers["X-OpenAI-API-Key"] = current_model_settings["openai_api_key"] + elif model == "grok-4-1" and current_model_settings["xai_api_key"]: + headers["X-XAI-API-Key"] = current_model_settings["xai_api_key"] + elif model in ["claude-4.5", "claude-opus"] and current_model_settings["anthropic_api_key"]: + headers["X-Anthropic-API-Key"] = current_model_settings["anthropic_api_key"] + elif model == "gemini-3" and current_model_settings["google_api_key"]: + headers["X-Google-API-Key"] = current_model_settings["google_api_key"] + + # Tavily API key for web research (optional - AI model used as fallback) + if current_model_settings["tavily_api_key"]: + headers["X-Tavily-API-Key"] = current_model_settings["tavily_api_key"] + + return headers + +def chat_with_agent_streaming(message: str, history: list, initialized_history: list = None) -> Generator: + """ + Send a message to the EU AI Act agent and stream the response with tool calls + + Args: + message: User's input message + history: Original chat history for API (without current user message) + initialized_history: Pre-initialized history with user message and loading (optional) + + Yields: + Updated history with streaming content + """ + global cancel_token + + if not message.strip(): + yield initialized_history or history + return + + # Reset cancellation token for new request + cancel_token.reset() + + # Use pre-initialized history or create one + if initialized_history: + new_history = list(initialized_history) + else: + new_history = list(history) + [ + ChatMessage(role="user", content=message), + ChatMessage(role="assistant", content="⏳ *Thinking...*") + ] + + response = None + bot_response = "" + tool_calls_content = "" # All tool calls, results, and thinking sections (in order) + current_thinking = "" # Accumulate thinking text before tool calls + + try: + # Convert original history to API format (handle both ChatMessage and dict) + api_history = [] + for msg in history: + if isinstance(msg, dict): + api_history.append({"role": msg.get("role", "user"), "content": msg.get("content", "")}) + else: + api_history.append({"role": msg.role, "content": msg.content}) + + # Make streaming request to API with model configuration headers + response = requests.post( + f"{API_URL}/api/chat", + json={"message": message, "history": api_history}, + headers=get_api_headers(), + stream=True, + timeout=API_TIMEOUT, + ) + + # Register response for potential cancellation + cancel_token.set_response(response) + + if response.status_code != 200: + error_msg = f"⚠️ Error: API returned status {response.status_code}" + new_history[-1] = ChatMessage(role="assistant", content=error_msg) + yield new_history + return + + # Initialize assistant response + current_tool_call = None + + for line in response.iter_lines(): + # Check for cancellation + if cancel_token.is_cancelled(): + # Include any accumulated thinking before cancellation + if current_thinking.strip(): + tool_calls_content += format_thinking_section(current_thinking) + + final_content = tool_calls_content + bot_response + if final_content: + final_content += "\n\n*— Execution stopped by user*" + else: + final_content = "*— Execution stopped by user*" + new_history[-1] = ChatMessage(role="assistant", content=final_content) + yield new_history + return + + if line: + line_str = line.decode('utf-8') + print(f"[DEBUG] Received: {line_str[:100]}...") # Debug log + if line_str.startswith('data: '): + try: + data = json.loads(line_str[6:]) # Remove 'data: ' prefix + event_type = data.get("type") + print(f"[DEBUG] Event type: {event_type}, data: {str(data)[:100]}") + + if event_type == "thinking": + # Handle thinking/reasoning tokens from Claude or GPT + # Show thinking at the END (bottom) where action is happening + thinking_content = data.get("content", "") + if thinking_content: + current_thinking += thinking_content + + # Show thinking tokens in real-time AT THE BOTTOM + # Tool calls first, then current thinking at the end + live_thinking = f"\n\n🧠 **Model Thinking (live):**\n\n```\n{current_thinking}\n```" + full_content = tool_calls_content + live_thinking + + new_history[-1] = ChatMessage(role="assistant", content=full_content) + yield new_history + + elif event_type == "text": + # Append text chunk + text_content = data.get("content", "") + text_phase = data.get("phase", "thinking") # Server tells us the phase + has_had_tools = data.get("hasHadToolCalls", False) + + # Determine if this is "thinking" text or final response based on server phase + if text_phase == "thinking" or (not has_had_tools and not tool_calls_content): + # This is thinking text (before tool calls or between them) + current_thinking += text_content + + # Show thinking AT THE BOTTOM after tool calls + if not tool_calls_content: + # Initial thinking - show with brain indicator + display_content = f"🧠 **AI is reasoning...**\n\n{current_thinking}" + else: + # Thinking between tool calls - show at the end + display_content = tool_calls_content + f"\n\n🧠 *Reasoning:* {current_thinking}" + + new_history[-1] = ChatMessage(role="assistant", content=display_content) + else: + # This is "potential_response" - text after tool results + # Could be final response OR thinking before another tool call + # We accumulate it and will format appropriately when we know more + current_thinking += text_content + + # Show as streaming response AT THE BOTTOM + full_content = tool_calls_content + f"\n\n{current_thinking}" + new_history[-1] = ChatMessage(role="assistant", content=full_content) + + yield new_history + + elif event_type == "tool_call": + # Before showing tool call, save any accumulated thinking as collapsible + tool_name = data.get("toolName", "unknown") + args = data.get("args", {}) + + # If we have accumulated thinking text, add it as collapsible BEFORE this tool call + if current_thinking.strip(): + tool_calls_content += format_thinking_section(current_thinking, tool_name) + current_thinking = "" # Reset for next thinking block + else: + # No thinking text was output - add a synthetic thinking note + tool_display = tool_name.replace('_', ' ').title() + synthetic_thinking = f"I'll use the **{tool_display}** tool to gather the necessary information." + tool_calls_content += format_thinking_section(synthetic_thinking, tool_name) + + # Show tool call with prominent loading indicator AT THE BOTTOM + tool_calls_content += format_tool_call(tool_name, args) + # Add prominent loading indicator specific to this tool + loading_indicator = format_thinking_indicator(tool_name) + full_content = tool_calls_content + bot_response + loading_indicator + new_history[-1] = ChatMessage(role="assistant", content=full_content) + yield new_history + current_tool_call = tool_name + + elif event_type == "tool_result": + # Show tool result (removes loading indicator) + tool_name = data.get("toolName", current_tool_call or "unknown") + result = data.get("result") + tool_calls_content += format_tool_result(tool_name, result) + + # After tool result, show "analyzing results" indicator AT THE BOTTOM + analyzing_indicator = f"\n\n🧠 **Analyzing {tool_name.replace('_', ' ')} results...**\n" + full_content = tool_calls_content + analyzing_indicator + new_history[-1] = ChatMessage(role="assistant", content=full_content) + yield new_history + current_tool_call = None + + elif event_type == "step_finish": + # Step completed - if there's accumulated thinking, add it to tool_calls_content + has_had_tools_in_step = data.get("hasHadToolCalls", False) + + if current_thinking.strip(): + tool_calls_content += format_thinking_section(current_thinking) + current_thinking = "" + + # Show "preparing response" if we had tool calls and step is finishing AT THE BOTTOM + if has_had_tools_in_step and tool_calls_content: + preparing_indicator = "\n\n✨ **Preparing comprehensive response based on analysis...**\n" + full_content = tool_calls_content + preparing_indicator + else: + full_content = tool_calls_content + bot_response + + new_history[-1] = ChatMessage(role="assistant", content=full_content) + yield new_history + + elif event_type == "error": + error_msg = data.get("error", "Unknown error") + bot_response += f"\n\n⚠️ Error: {error_msg}" + full_content = tool_calls_content + bot_response + new_history[-1] = ChatMessage(role="assistant", content=full_content) + yield new_history + + elif event_type == "done": + # Final update + # If we have tool calls, any remaining current_thinking is the final response + # If no tool calls, current_thinking was just direct response (no tools needed) + if tool_calls_content: + # We had tool calls - current_thinking after last tool is the final response + bot_response = current_thinking + current_thinking = "" + else: + # No tool calls - current_thinking is the direct response + bot_response = current_thinking + current_thinking = "" + + # Final response AT THE BOTTOM after all tool calls + full_content = tool_calls_content + bot_response + new_history[-1] = ChatMessage(role="assistant", content=full_content) + yield new_history + break + + except json.JSONDecodeError: + continue + + # Ensure final state (only if not cancelled) + if not cancel_token.is_cancelled(): + # If we have accumulated text that wasn't finalized, treat it as the response + if current_thinking.strip() and not bot_response: + bot_response = current_thinking + + # Final content: tool calls first, then response at the bottom + final_content = tool_calls_content + (bot_response or "No response generated.") + new_history[-1] = ChatMessage(role="assistant", content=final_content) + yield new_history + + except requests.exceptions.ConnectionError: + if not cancel_token.is_cancelled(): + error_msg = "⚠️ Cannot connect to API server. Make sure it's running on http://localhost:3001" + new_history[-1] = ChatMessage(role="assistant", content=error_msg) + yield new_history + except requests.exceptions.Timeout: + if not cancel_token.is_cancelled(): + error_msg = "⚠️ Request timed out. The agent might be processing a complex query." + final_content = tool_calls_content + bot_response + "\n\n" + error_msg + new_history[-1] = ChatMessage(role="assistant", content=final_content) + yield new_history + except (requests.exceptions.ChunkedEncodingError, ConnectionError): + # This can happen when we close the connection during cancellation - it's expected + if not cancel_token.is_cancelled(): + error_msg = "⚠️ Connection was interrupted." + final_content = tool_calls_content + bot_response + "\n\n" + error_msg + new_history[-1] = ChatMessage(role="assistant", content=final_content) + yield new_history + except Exception as e: + if not cancel_token.is_cancelled(): + error_msg = f"⚠️ Error: {str(e)}" + final_content = tool_calls_content + bot_response + "\n\n" + error_msg if (tool_calls_content or bot_response) else error_msg + new_history[-1] = ChatMessage(role="assistant", content=final_content) + yield new_history + finally: + # Clean up the response connection + if response is not None: + try: + response.close() + except: + pass + cancel_token.set_response(None) + +def check_api_status() -> str: + """Check if the API server is running""" + try: + response = requests.get(f"{API_URL}/health", timeout=5) + if response.status_code == 200: + data = response.json() + return f"✅ API Server: {data.get('service')} v{data.get('version')}" + else: + return f"⚠️ API Server returned status {response.status_code}" + except requests.exceptions.ConnectionError: + return "❌ API Server not running. Start it with: pnpm dev" + except Exception as e: + return f"❌ Error: {str(e)}" + +def get_available_tools() -> str: + """Get list of available MCP tools with descriptions""" + try: + response = requests.get(f"{API_URL}/api/tools", timeout=5) + if response.status_code == 200: + data = response.json() + tools = data.get("tools", []) + if tools: + tool_list = "\n".join([f"• **{t['name']}**" for t in tools]) + return f"""**Available MCP Tools:** + +{tool_list} + +**✨ Capabilities:** +• Generate complete compliance reports +• Create documentation templates (Risk Management, Technical Docs, etc.) +• Discover AI systems and assess risk levels +• Analyze organization compliance gaps""" + return "No tools available" + return "Could not fetch tools" + except: + return "Could not connect to API" + +def get_example_queries() -> List[List[str]]: + """Get example queries for the interface""" + return [ + # MCP Tools Examples - Showcase full compliance analysis capabilities + ["Generate a complete EU AI Act compliance report for Microsoft with all documentation templates"], + ["Analyze IBM's watsonX system compliance and generate risk management documentation"], + ["Create full compliance assessment for OpenAI including technical documentation templates"], + # General Questions + ["What is the EU AI Act?"], + ["Is a recruitment screening AI considered high-risk?"], + ["What are the compliance requirements for chatbots?"], + ["What's the timeline for EU AI Act enforcement?"], + ] + +# Default browser state for persistent storage +DEFAULT_BROWSER_STATE = { + "api_keys": { + "tavily": "", + "anthropic": "", + "google": "", + "openai": "", + "xai": "" + }, + "model": "gpt-oss" +} + +# Create Gradio interface +with gr.Blocks( + title="🇪🇺 EU AI Act Compliance Agent", +) as demo: + # Browser state for persistent storage (persists across page refreshes) + browser_state = gr.BrowserState(DEFAULT_BROWSER_STATE) + + # Custom CSS only - JavaScript is loaded via gr.Blocks(js=...) parameter + gr.HTML(""" + + """) + + # Header - use PUBLIC_URL for production links + # In production (HF Spaces): Show info about gradio.live URL + # In local dev: Direct link to localhost:7861 + chatgpt_link_href = PUBLIC_URL if PUBLIC_URL else "http://localhost:7861" + is_production = bool(PUBLIC_URL) + + # Get MCP URL (written by chatgpt_app.py when it starts) + mcp_url = get_mcp_url() + + # MCP Server is deployed separately + MCP_SPACE_URL = "https://mcp-1st-birthday-eu-ai-act-chatgpt-mcp.hf.space" + MCP_URL = f"{MCP_SPACE_URL}/gradio_api/mcp/" + + if is_production: + # Production: Link to separate MCP Space + chatgpt_section = f""" + +
+ + 💬 ChatGPT MCP Server + +

+ {MCP_URL} +

+

+ Click to open MCP Server Space → +

+
+
+ """ + else: + # Local dev: Direct link + chatgpt_section = """ + + 💬 Open ChatGPT App (MCP Server) + + """ + + gr.HTML(f""" +
+

🇪🇺 EU AI Act Compliance Agent

+

by Legitima.ai

+

Your intelligent assistant for navigating European AI regulation

+

+ {chatgpt_section} +

+
+ """) + + # Main content + with gr.Row(): + with gr.Column(scale=3): + # Chat interface - using ChatMessage format + chatbot = gr.Chatbot( + label="Chat with EU AI Act Expert", + height=550, + show_label=True, + autoscroll=False, # Disable auto-scroll - we handle it with JS + ) + + with gr.Row(): + msg = gr.Textbox( + placeholder="Ask about compliance, or request a full compliance report with documentation for an organization...", + show_label=False, + scale=8, + ) + submit = gr.Button("Send", variant="primary", scale=1) + stop_btn = gr.Button("⏹ Stop", variant="stop", scale=1, visible=False) + + gr.Examples( + examples=get_example_queries(), + inputs=msg, + label="💡 Example Questions (Try MCP tools for compliance reports & documentation!)", + ) + + with gr.Column(scale=1): + # Sidebar + gr.Markdown("### 🤖 Model Settings") + + model_dropdown = gr.Dropdown( + choices=[(v["name"], k) for k, v in AVAILABLE_MODELS.items()], + value=current_model_settings["model"], # Use current model setting + label="AI Model", + info="Select the AI model to use. ⚠️ GPT-OSS is FREE but may take up to 60s to start (cold start). For faster responses and better precision, use another model with your API key.", + elem_id="model_dropdown" + ) + + # API Key inputs (password fields) - GPT-OSS is FREE, other models require API keys + with gr.Accordion("🔑 API Keys & Settings", open=True): + gr.Markdown("""🆓 **GPT-OSS 20B is FREE** - Uses pre-configured Modal endpoint (no setup required). + +⏱️ **Note:** GPT-OSS may take up to **60 seconds** to start responding due to cold start. For **faster responses and better precision**, select another model and provide your API key below. + +⚠️ For paid models (Claude, GPT-5, Gemini, Grok), an API key is required. + +🔐 Keys are stored securely in encoded cookies and **auto-expire after 24 hours**. + +ℹ️ *Tavily is **optional** - enhances web research for organization & AI systems discovery. Falls back to server's `TAVILY_API_KEY` env var if not provided, then to AI model.*""") + + gr.Markdown("#### 🔍 Research API (Optional)") + tavily_key = gr.Textbox( + label="Tavily API Key (Optional)", + placeholder="tvly-... (optional - enhances web research)", + type="password", + value="", # Will be populated from cookies via JS + info="Optional - uses server env var fallback, then AI model.", + elem_id="tavily_key_input" + ) + + gr.Markdown("#### 🤖 AI Model APIs") + anthropic_key = gr.Textbox( + label="Anthropic API Key *", + placeholder="sk-ant-... (required for Claude models)", + type="password", + value="", # Will be populated from cookies via JS + info="Required - for Claude 4.5 Sonnet or Claude Opus 4", + elem_id="anthropic_key_input" + ) + google_key = gr.Textbox( + label="Google API Key", + placeholder="AIza... (required for Gemini 3)", + type="password", + value="", # Will be populated from cookies via JS + info="Required if using Gemini 3 Pro model", + elem_id="google_key_input" + ) + openai_key = gr.Textbox( + label="OpenAI API Key", + placeholder="sk-... (required for GPT-5)", + type="password", + value="", # Will be populated from cookies via JS + info="Required if using GPT-5 model", + elem_id="openai_key_input" + ) + xai_key = gr.Textbox( + label="xAI API Key", + placeholder="xai-... (required for Grok 4.1)", + type="password", + value="", # Will be populated from cookies via JS + info="Required if using Grok 4.1 model", + elem_id="xai_key_input" + ) + with gr.Row(): + save_keys_btn = gr.Button("💾 Save Keys", variant="secondary", size="sm") + clear_keys_btn = gr.Button("🗑️ Clear Keys", variant="stop", size="sm") + keys_status = gr.Markdown("") + + gr.Markdown("---") + + gr.Markdown("### 📊 Quick Reference") + + gr.Markdown(""" +**Risk Categories:** +- 🔴 **Unacceptable** - Banned +- 🟠 **High Risk** - Strict requirements +- 🟡 **Limited Risk** - Transparency +- 🟢 **Minimal Risk** - No obligations + +**Key Deadlines:** +- 📅 Feb 2, 2025: Banned AI +- 📅 Aug 2, 2026: High-risk rules +- 📅 Aug 2, 2027: Full enforcement + """) + + gr.Markdown("---") + + tools_info = gr.Markdown( + value=get_available_tools(), + label="🔧 MCP Tools - Generate Reports & Documentation" + ) + + gr.Markdown("---") + + # Sidebar ChatGPT App section + if is_production: + sidebar_chatgpt = f""" + +
+ 💬 MCP Server +

+ Click to get MCP URL → +

+
+
+ """ + else: + sidebar_chatgpt = """ + + 💬 ChatGPT App (MCP Server) + +

+ Use the ChatGPT App to connect with ChatGPT Desktop and access MCP tools via OpenAI Apps SDK. +

+ """ + + gr.HTML(f""" +
+

🔗 Other Interfaces

+
+ {sidebar_chatgpt} +
+
+ """) + + gr.Markdown("---") + + status = gr.Textbox( + label="🔌 API Status", + value=check_api_status(), + interactive=False, + max_lines=2, + ) + + with gr.Row(): + refresh_btn = gr.Button("🔄 Refresh", size="sm") + clear_btn = gr.Button("🗑️ Clear", size="sm") + + # Footer + gr.Markdown(""" +--- +
+

Built for the MCP 1st Birthday Hackathon 🎂

+

Powered by Vercel AI SDK v5 + Model Context Protocol + Gradio

+
+ """) + + # Disclaimer box - separate for better visibility + gr.HTML(""" + +
+

+ ⚠️ Disclaimer: This is a demo application (Work in Progress) and does not constitute legal advice. +

+

+ Always consult with qualified legal professionals before making compliance decisions based on AI outputs. +

+
+ """) + + # Model and API key handlers + def update_model(model_value): + """Update the selected model""" + current_model_settings["model"] = model_value + model_info = AVAILABLE_MODELS.get(model_value, {}) + print(f"[Gradio] Model updated to: {model_value} ({model_info.get('name', model_value)})") + return f"✅ Model set to: **{model_info.get('name', model_value)}**" + + def save_api_keys(tavily_val, anthropic_val, google_val, openai_val, xai_val): + """Save user-provided API keys to session AND secure cookie storage + + SECURITY: Keys are stored in memory for this session AND in encoded cookies + that expire after 1 day. Cookies use XOR obfuscation + base64 encoding. + GPT-OSS is FREE and uses pre-configured Modal endpoint from environment. + Paid models require API keys. Note: Tavily is OPTIONAL - AI model is used as fallback for research. + """ + saved = [] + + # Only update if a real key is provided + if tavily_val and len(tavily_val) > 10: + current_model_settings["tavily_api_key"] = tavily_val + saved.append("Tavily") + + if anthropic_val and len(anthropic_val) > 10: + current_model_settings["anthropic_api_key"] = anthropic_val + saved.append("Anthropic") + + if google_val and len(google_val) > 10: + current_model_settings["google_api_key"] = google_val + saved.append("Google") + + if openai_val and len(openai_val) > 10: + current_model_settings["openai_api_key"] = openai_val + saved.append("OpenAI") + + if xai_val and len(xai_val) > 10: + current_model_settings["xai_api_key"] = xai_val + saved.append("xAI") + + # Build status message + status_parts = [] + + # Always show current model + model = current_model_settings["model"] + model_info = AVAILABLE_MODELS.get(model, {}) + status_parts.append(f"🤖 **Model:** {model_info.get('name', model)}") + + if saved: + status_parts.append(f"✅ **Keys Saved:** {', '.join(saved)}") + + status_parts.append("🔐 *Settings stored in secure cookies (expires in 24h)*") + + # Check for missing required keys based on selected model + if model == "gpt-oss": + # GPT-OSS uses hardcoded Modal endpoint - always available + status_parts.append("🆓 *GPT-OSS model is FREE - no API key required!*") + elif model in ["claude-4.5", "claude-opus"] and not current_model_settings["anthropic_api_key"]: + status_parts.append(f"⚠️ **Anthropic API key required** for {model}") + elif model == "gemini-3" and not current_model_settings["google_api_key"]: + status_parts.append("⚠️ **Google API key required** for Gemini 3") + elif model == "gpt-5" and not current_model_settings["openai_api_key"]: + status_parts.append("⚠️ **OpenAI API key required** for GPT-5") + elif model == "grok-4-1" and not current_model_settings["xai_api_key"]: + status_parts.append("⚠️ **xAI API key required** for Grok 4.1") + + # Tavily is optional - just inform user about enhanced features if they have it + if not current_model_settings["tavily_api_key"]: + status_parts.append("ℹ️ *Tavily not set - will use server env var fallback or AI model*") + + return "\n\n".join(status_parts) + + def get_current_model_status(): + """Get current model and key status""" + model = current_model_settings["model"] + model_info = AVAILABLE_MODELS.get(model, {}) + required_key = model_info.get("api_key_env", "") + + key_status = "❌ Missing" + if required_key == "OPENAI_API_KEY" and current_model_settings["openai_api_key"]: + key_status = "✅ Set" + elif required_key == "XAI_API_KEY" and current_model_settings["xai_api_key"]: + key_status = "✅ Set" + elif required_key == "ANTHROPIC_API_KEY" and current_model_settings["anthropic_api_key"]: + key_status = "✅ Set" + + return f"**Model:** {model_info.get('name', model)}\n**Key Status:** {key_status}" + + def check_required_keys(): + """Check if required API keys are configured + + Returns a tuple of (is_valid, error_message) + - is_valid: True if all required keys are present + - error_message: Description of missing keys if not valid + + Note: GPT-OSS is FREE and no API key is required. + Tavily API key is optional - the system will fallback to AI model for research. + """ + missing_keys = [] + + # Check model API key based on selected model + model = current_model_settings["model"] + if model == "gpt-oss": + # GPT-OSS uses hardcoded endpoint - no check needed + pass + elif model in ["claude-4.5", "claude-opus"] and not current_model_settings["anthropic_api_key"]: + missing_keys.append(f"**Anthropic API Key** (required for {model})") + elif model == "gemini-3" and not current_model_settings["google_api_key"]: + missing_keys.append("**Google API Key** (required for Gemini 3)") + elif model == "gpt-5" and not current_model_settings["openai_api_key"]: + missing_keys.append("**OpenAI API Key** (required for GPT-5)") + elif model == "grok-4-1" and not current_model_settings["xai_api_key"]: + missing_keys.append("**xAI API Key** (required for Grok 4.1)") + + # Note: Tavily is OPTIONAL - system will fallback to AI model for research + # We no longer require Tavily API key + + if missing_keys: + error_msg = """## ⚠️ API Keys Required + +To use this service, you need to provide your own API keys. The following keys are missing: + +""" + for key in missing_keys: + error_msg += f"- {key}\n" + + error_msg += """ +### How to add your API keys: + +1. Expand the **🔑 API Keys & Settings** section in the sidebar +2. Enter your API keys in the corresponding fields +3. Click **💾 Save Keys** + +### Where to get API keys: + +- **Anthropic**: [console.anthropic.com](https://console.anthropic.com) - Get Claude API key +- **Google**: [aistudio.google.com](https://aistudio.google.com/apikey) - Get Gemini API key +- **OpenAI**: [platform.openai.com](https://platform.openai.com) - Get GPT API key +- **xAI**: [console.x.ai](https://console.x.ai) - Get Grok API key + +**🆓 FREE Alternative:** +- Select **GPT-OSS 20B** from the model dropdown - it's FREE via Modal.com! + +**Optional:** +- **Tavily**: [tavily.com](https://tavily.com) - For enhanced web research (falls back to server env var, then AI model) +""" + return False, error_msg + + return True, "" + + # Event handlers - clear input immediately and stream response together + def respond_and_clear(message: str, history: list): + """Wrapper that yields (cleared_input, chat_history, stop_visible) tuples""" + global cancel_token + + if not message.strip(): + yield "", history, gr.update(visible=False) + return + + # Check for required API keys before proceeding + keys_valid, error_message = check_required_keys() + if not keys_valid: + # Show user message and error about missing keys + error_history = list(history) + [ + ChatMessage(role="user", content=message), + ChatMessage(role="assistant", content=error_message) + ] + yield "", error_history, gr.update(visible=False) + return + + # Reset cancellation token for new request + cancel_token.reset() + + # First yield: clear input, show loading, and show stop button + # Get the actual selected model from current_model_settings (not from env) + selected_model = current_model_settings.get("model", "gpt-oss") + model_info = AVAILABLE_MODELS.get(selected_model, {}) + model_name = model_info.get("name", selected_model) + print(f"[Gradio] Using model: {selected_model} ({model_name})") + initial_history = list(history) + [ + ChatMessage(role="user", content=message), + ChatMessage(role="assistant", content=f"⏳ *Thinking with {model_name}...*") + ] + yield "", initial_history, gr.update(visible=True) + + # Stream the actual response (pass initialized_history to avoid duplication) + updated_history = initial_history # Initialize in case generator doesn't yield + for updated_history in chat_with_agent_streaming(message, history, initial_history): + # Check if cancelled during streaming + if cancel_token.is_cancelled(): + break + yield "", updated_history, gr.update(visible=True) + + # Final yield: hide stop button when done + yield "", updated_history, gr.update(visible=False) + + def stop_response(history: list): + """Stop the current response by triggering cancellation""" + global cancel_token + + # Trigger cancellation - this will close the HTTP connection + cancel_token.cancel() + + # Update history to show stopped state (the generator will also update when it detects cancellation) + if history and len(history) > 0: + last_msg = history[-1] + if isinstance(last_msg, ChatMessage): + content = last_msg.content + # Remove thinking indicator and add stopped message + if "⏳" in content: + content = content.replace("⏳ *Thinking", "⏹️ *Stopped") + content = content.replace("⏳ *Processing", "⏹️ *Stopped") + if "*— Execution stopped by user*" not in content: + content += "\n\n*— Execution stopped by user*" + history[-1] = ChatMessage(role="assistant", content=content) + + # Return history and hide stop button + return history, gr.update(visible=False) + + # On submit/click: clear input immediately while streaming response + # These events are cancellable by the stop button + submit_event = msg.submit(respond_and_clear, [msg, chatbot], [msg, chatbot, stop_btn]) + click_event = submit.click(respond_and_clear, [msg, chatbot], [msg, chatbot, stop_btn]) + + # Stop button cancels the streaming events and updates the chat + stop_btn.click( + fn=stop_response, + inputs=[chatbot], + outputs=[chatbot, stop_btn], + cancels=[submit_event, click_event] + ) + + refresh_btn.click( + lambda: (check_api_status(), get_available_tools()), + None, + [status, tools_info] + ) + clear_btn.click(lambda: [], None, chatbot) + + # Function to update model in browser state + def save_model_to_browser_state(model_val, stored_data): + """Save model selection to browser state""" + if stored_data is None: + stored_data = DEFAULT_BROWSER_STATE.copy() + new_data = stored_data.copy() + new_data["model"] = model_val or "gpt-oss" + print(f"[BrowserState] Model changed to: {model_val}") + return new_data + + # Model selection handler - also saves to browser state + model_dropdown.change( + update_model, + [model_dropdown], + [keys_status] + ).then( + save_model_to_browser_state, + [model_dropdown, browser_state], + [browser_state] + ) + + # Function to save settings to browser state + def save_to_browser_state(tavily_val, anthropic_val, google_val, openai_val, xai_val, model_val, stored_data): + """Save API keys and model to browser state (persists across refreshes)""" + new_data = { + "api_keys": { + "tavily": tavily_val or "", + "anthropic": anthropic_val or "", + "google": google_val or "", + "openai": openai_val or "", + "xai": xai_val or "" + }, + "model": model_val or "gpt-oss" + } + print(f"[BrowserState] Saving to browser: model={model_val}, keys={[k for k,v in new_data['api_keys'].items() if v]}") + return new_data + + # Combined save handler - saves to session AND browser state + save_keys_btn.click( + save_api_keys, + [tavily_key, anthropic_key, google_key, openai_key, xai_key], + [keys_status] + ).then( + save_to_browser_state, + [tavily_key, anthropic_key, google_key, openai_key, xai_key, model_dropdown, browser_state], + [browser_state] + ) + + # Clear keys function - clears both session and cookies, resets model to default + def clear_api_keys(): + """Clear all stored API keys from session and cookies, reset model to default""" + # Note: modal_endpoint_url is hardcoded, so we don't clear it + current_model_settings["tavily_api_key"] = "" + current_model_settings["anthropic_api_key"] = "" + current_model_settings["google_api_key"] = "" + current_model_settings["openai_api_key"] = "" + current_model_settings["xai_api_key"] = "" + current_model_settings["model"] = "gpt-oss" # Reset to default FREE model + # Return: tavily, anthropic, google, openai, xai, model_value, status + return "", "", "", "", "", "gpt-oss", "🗑️ All settings cleared (model reset to GPT-OSS)" + + # Function to clear browser state + def clear_browser_state(): + """Clear all stored data from browser state""" + print("[BrowserState] Clearing all stored data") + return DEFAULT_BROWSER_STATE + + # Combined clear handler - clears session AND browser state + clear_keys_btn.click( + clear_api_keys, + [], + [tavily_key, anthropic_key, google_key, openai_key, xai_key, model_dropdown, keys_status] + ).then( + clear_browser_state, + [], + [browser_state] + ) + + # === Load handler: Restore API keys and model from browser storage on page load === + def load_from_browser_state(stored_data): + """Load API keys and model from browser storage (runs on page load) + + Returns: (tavily, anthropic, google, openai, xai, model, status_message) + """ + if not stored_data: + print("[BrowserState] No stored data found") + return "", "", "", "", "", "gpt-oss", "🔧 Ready - configure API keys to get started" + + api_keys = stored_data.get("api_keys", {}) + model = stored_data.get("model", "gpt-oss") + + # Extract individual keys + tavily = api_keys.get("tavily", "") + anthropic = api_keys.get("anthropic", "") + google = api_keys.get("google", "") + openai = api_keys.get("openai", "") + xai = api_keys.get("xai", "") + + # Check which keys were loaded + loaded_keys = [k for k, v in api_keys.items() if v] + + if loaded_keys or model != "gpt-oss": + print(f"[BrowserState] Restoring: model={model}, keys={loaded_keys}") + + # Also update the session state + current_model_settings["model"] = model + if tavily: + current_model_settings["tavily_api_key"] = tavily + if anthropic: + current_model_settings["anthropic_api_key"] = anthropic + if google: + current_model_settings["google_api_key"] = google + if openai: + current_model_settings["openai_api_key"] = openai + if xai: + current_model_settings["xai_api_key"] = xai + + # Build status message + model_info = AVAILABLE_MODELS.get(model, {}) + status_parts = [] + status_parts.append(f"🤖 **Model:** {model_info.get('name', model)}") + + if loaded_keys: + status_parts.append(f"🔐 **Restored from browser:** {', '.join(loaded_keys)}") + + if model == "gpt-oss": + status_parts.append("🆓 *GPT-OSS model is FREE - no API key required!*") + + status = "\n\n".join(status_parts) + else: + print("[BrowserState] No saved settings to restore") + status = "🔧 Ready - configure API keys to get started" + + return tavily, anthropic, google, openai, xai, model, status + + # Trigger on page load - restore saved settings from browser storage + demo.load( + load_from_browser_state, + inputs=[browser_state], + outputs=[tavily_key, anthropic_key, google_key, openai_key, xai_key, model_dropdown, keys_status] + ) + +# Launch the app +if __name__ == "__main__": + print("\n" + "="*60) + print("🇪🇺 EU AI Act Compliance Agent - Gradio UI") + print("="*60) + print(f"\n📡 API Server: {API_URL}") + print(f"✓ Status: {check_api_status()}") + print(f"\n🚀 Starting Gradio interface...") + print("="*60 + "\n") + + demo.launch( + server_name=os.getenv("GRADIO_SERVER_NAME", "0.0.0.0"), + server_port=int(os.getenv("GRADIO_SERVER_PORT", "7860")), + share=os.getenv("GRADIO_SHARE", "false").lower() == "true", + show_error=True, + ) diff --git a/apps/eu-ai-act-agent/src/server.ts b/apps/eu-ai-act-agent/src/server.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5b9f1763421cf4da64739d8bf94f8bbc10374a6 --- /dev/null +++ b/apps/eu-ai-act-agent/src/server.ts @@ -0,0 +1,1235 @@ +#!/usr/bin/env node + +/** + * EU AI Act Compliance Agent Server + * Express API with Vercel AI SDK v5 agent + * + * Supports streaming text and tool calls + */ + +import express from "express"; +import cors from "cors"; +import { config } from "dotenv"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { createAgent } from "./agent/index.js"; +import { + discoverOrganization, + discoverAIServices, + assessCompliance, + type ApiKeys, +} from "@eu-ai-act/mcp-server"; + +// Load environment variables from project root +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +config({ path: resolve(__dirname, "../../../.env") }); // Go up from src -> eu-ai-act-agent -> apps -> root + +const app = express(); +const PORT = process.env.PORT || 3001; + +// Middleware +app.use( + cors({ + origin: [ + "http://localhost:7860", + "http://127.0.0.1:7860", + "http://localhost:3000", + ], + credentials: true, + }), +); +app.use(express.json()); + +import { readFileSync, existsSync } from "fs"; + +// Health check +app.get("/health", (_req, res) => { + res.json({ + status: "ok", + service: "EU AI Act Compliance Agent", + version: "0.1.0", + }); +}); + +// MCP URL endpoint - returns the gradio.live URL for ChatGPT integration +app.get("/api/mcp-url", (_req, res) => { + try { + const mcpUrlFile = resolve(__dirname, ".mcp_url"); + if (existsSync(mcpUrlFile)) { + const url = readFileSync(mcpUrlFile, "utf-8").trim(); + res.json({ url, status: "ready" }); + } else { + res.json({ url: null, status: "starting" }); + } + } catch (error) { + res.json({ url: null, status: "error", error: String(error) }); + } +}); + +/** + * Process stream events and write to response + * Returns set of tool names that were called + * + * Tracks "thinking" phases - text that appears before tool calls + * vs "response" text that appears after all tools complete + */ +async function processStreamEvents( + stream: AsyncIterable, + res: express.Response, +): Promise<{ + toolsCalled: Set; + toolResults: Map; + hasText: boolean; +}> { + const toolsCalled = new Set(); + const toolResults = new Map(); + let hasText = false; + + // Track phase for thinking vs response text + let pendingToolCall = false; // True when we've seen a tool_call but not its result yet + let hasHadToolCalls = false; // True once we've seen at least one tool call + + for await (const event of stream) { + // Log all non-text events for debugging + if (event.type !== "text-delta") { + console.log( + "Stream event:", + event.type, + JSON.stringify(event).substring(0, 200), + ); + } else { + hasText = true; + } + + switch (event.type) { + // Handle reasoning/thinking tokens from Claude and GPT + // Claude uses "reasoning" with textDelta, OpenAI may use different formats + case "reasoning": + const reasoningText = (event as any).textDelta ?? ""; + if (reasoningText) { + console.log("[THINKING]", reasoningText.substring(0, 100)); + res.write( + `data: ${JSON.stringify({ + type: "thinking", + content: reasoningText, + })}\n\n`, + ); + } + break; + + // Handle reasoning signature (Claude's thinking summary) + case "reasoning-signature": + const signatureText = (event as any).signature ?? ""; + if (signatureText) { + console.log("[THINKING SIGNATURE]", signatureText.substring(0, 100)); + res.write( + `data: ${JSON.stringify({ + type: "thinking", + content: `[Reasoning Summary] ${signatureText}`, + })}\n\n`, + ); + } + break; + + // Handle redacted reasoning (when thinking is hidden) + case "redacted-reasoning": + console.log("[REDACTED REASONING]"); + res.write( + `data: ${JSON.stringify({ + type: "thinking", + content: "[Model is reasoning internally...]", + })}\n\n`, + ); + break; + + case "text-delta": + const textContent = + (event as any).textDelta ?? + (event as any).delta ?? + (event as any).text ?? + ""; + + // Determine if this is thinking or response text + // Text before any tool call = thinking + // Text between tool result and next tool call = thinking + // Text after last tool result with no more tool calls = response (we can't know this yet, so we mark it as potential_response) + const textPhase = + hasHadToolCalls && !pendingToolCall + ? "potential_response" + : "thinking"; + + res.write( + `data: ${JSON.stringify({ + type: "text", + content: textContent, + phase: textPhase, + hasHadToolCalls, + })}\n\n`, + ); + break; + + case "tool-call": + console.log("TOOL CALL:", event.toolName); + hasHadToolCalls = true; + pendingToolCall = true; + + toolsCalled.add(event.toolName); + const toolArgs = (event as any).args ?? (event as any).input ?? {}; + res.write( + `data: ${JSON.stringify({ + type: "tool_call", + toolName: event.toolName, + toolCallId: event.toolCallId, + args: toolArgs, + })}\n\n`, + ); + break; + + case "tool-result": + console.log("TOOL RESULT:", event.toolName); + pendingToolCall = false; + + const toolOutput = (event as any).output; + const directResult = (event as any).result; + let parsedResult = null; + + if (directResult) { + parsedResult = directResult; + } else if (toolOutput?.content?.[0]?.text) { + try { + parsedResult = JSON.parse(toolOutput.content[0].text); + } catch { + parsedResult = toolOutput.content[0].text; + } + } + + toolResults.set(event.toolName, parsedResult); + + res.write( + `data: ${JSON.stringify({ + type: "tool_result", + toolName: event.toolName, + toolCallId: event.toolCallId, + result: parsedResult, + })}\n\n`, + ); + break; + + case "step-finish": + // When a step finishes, if we had tool calls and there's no pending tool, + // the next text will be response (or thinking for next tool) + res.write( + `data: ${JSON.stringify({ + type: "step_finish", + finishReason: event.finishReason, + hasHadToolCalls, + })}\n\n`, + ); + break; + + case "error": + res.write( + `data: ${JSON.stringify({ + type: "error", + error: String(event.error), + })}\n\n`, + ); + break; + } + } + + return { toolsCalled, toolResults, hasText }; +} + +// Main chat endpoint with full streaming support +app.post("/api/chat", async (req, res) => { + try { + const { message, history = [] } = req.body; + + if (!message || typeof message !== "string") { + return res.status(400).json({ error: "Message is required" }); + } + + // Read model selection and API keys from headers (set by Gradio UI) + // IMPORTANT: API keys are ONLY from user input via Gradio UI - NEVER from env vars! + const modelName = (req.headers["x-ai-model"] as string) || "gpt-oss"; + + // API keys from Gradio UI (stored in user's cookies) + const apiKeys = { + modalEndpointUrl: + (req.headers["x-modal-endpoint-url"] as string) || undefined, + openaiApiKey: (req.headers["x-openai-api-key"] as string) || undefined, + xaiApiKey: (req.headers["x-xai-api-key"] as string) || undefined, + anthropicApiKey: + (req.headers["x-anthropic-api-key"] as string) || undefined, + googleApiKey: (req.headers["x-google-api-key"] as string) || undefined, + }; + + // Tavily API key (optional - for web research) + const tavilyApiKey = + (req.headers["x-tavily-api-key"] as string) || undefined; + + console.log( + `[API] Model: ${modelName}, API keys provided: ${ + Object.entries(apiKeys) + .filter(([_, v]) => v) + .map(([k]) => k) + .join(", ") || "none (GPT-OSS is FREE)" + }`, + ); + if (tavilyApiKey) { + console.log( + `[API] Tavily API key provided: ${tavilyApiKey.substring(0, 10)}...`, + ); + } + + // For GPT-OSS, use default Modal endpoint if not provided + if (modelName === "gpt-oss" && !apiKeys.modalEndpointUrl) { + apiKeys.modalEndpointUrl = + "https://vasilis--gpt-oss-vllm-inference-serve.modal.run"; + } + + // Set headers for streaming + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + res.setHeader("X-Accel-Buffering", "no"); + + // Send user message confirmation immediately + res.write( + `data: ${JSON.stringify({ type: "user_message", content: message })}\n\n`, + ); + + // Create agent instance with model, API keys, and Tavily key from Gradio UI + const agent = createAgent({ modelName, apiKeys, tavilyApiKey }); + + // Convert history to messages format + let messages = history.map((msg: any) => ({ + role: msg.role, + content: msg.content, + })); + + // For GPT-OSS (smaller model), implement token-based trimming to avoid context overflow + // GPT-OSS 20B has 16K context window + const isGptOss = modelName === "gpt-oss"; + if (isGptOss && messages.length > 0) { + // Token budget calculation for GPT-OSS: + // - Context window: 16,384 tokens + // - System prompt: ~5,000 tokens (expanded with Article 6 guidelines) + // - Tool definitions: ~1,500 tokens + // - Output buffer (maxOutputTokens): 8,000 tokens (for comprehensive reports) + // - Safety margin: 300 tokens + // Available for history: ~1,584 tokens (~6 short messages) + const GPT_OSS_CONTEXT_WINDOW = 16384; + const SYSTEM_PROMPT_TOKENS = 5000; + const TOOL_DEFINITIONS_TOKENS = 1500; + const OUTPUT_BUFFER_TOKENS = 8000; + const SAFETY_MARGIN_TOKENS = 300; + const MAX_HISTORY_TOKENS = + GPT_OSS_CONTEXT_WINDOW - + SYSTEM_PROMPT_TOKENS - + TOOL_DEFINITIONS_TOKENS - + OUTPUT_BUFFER_TOKENS - + SAFETY_MARGIN_TOKENS; + + // Estimate tokens: ~4 characters per token (conservative estimate for English text) + const estimateTokens = (text: string): number => + Math.ceil((text || "").length / 4); + + // First, truncate very long individual messages (e.g., tool results) + const MAX_MESSAGE_CHARS = 1000; // ~250 tokens per message max (tight budget) + messages = messages.map((msg: any) => { + if (msg.content && msg.content.length > MAX_MESSAGE_CHARS) { + console.log( + `[API] GPT-OSS: Truncating long ${msg.role} message (${msg.content.length} chars → ${MAX_MESSAGE_CHARS} chars)`, + ); + return { + ...msg, + content: + msg.content.substring(0, MAX_MESSAGE_CHARS) + + "\n\n[...truncated for context limits...]", + }; + } + return msg; + }); + + // Calculate total history tokens + let totalHistoryTokens = messages.reduce( + (sum: number, msg: any) => sum + estimateTokens(msg.content), + 0, + ); + + // Also count tokens for the current message we're about to add + const currentMessageTokens = estimateTokens(message); + totalHistoryTokens += currentMessageTokens; + + console.log( + `[API] GPT-OSS: History tokens estimate: ${totalHistoryTokens} / ${MAX_HISTORY_TOKENS} max (${messages.length} messages)`, + ); + + // If over budget, trim oldest messages first + while (totalHistoryTokens > MAX_HISTORY_TOKENS && messages.length > 0) { + const removedMsg = messages.shift(); + const removedTokens = estimateTokens(removedMsg?.content || ""); + totalHistoryTokens -= removedTokens; + console.log( + `[API] GPT-OSS: Trimmed oldest ${removedMsg?.role} message (${removedTokens} tokens). New total: ${totalHistoryTokens}`, + ); + } + + // Hard limit: max 4 messages (2 turns) due to limited history budget + // With 8000 output tokens, we need to prioritize output over history + const MAX_HISTORY_MESSAGES = 4; + if (messages.length > MAX_HISTORY_MESSAGES) { + const trimCount = messages.length - MAX_HISTORY_MESSAGES; + console.log( + `[API] GPT-OSS: Trimming ${trimCount} messages to stay under ${MAX_HISTORY_MESSAGES} message limit`, + ); + messages = messages.slice(-MAX_HISTORY_MESSAGES); + } + + console.log( + `[API] GPT-OSS: Final history: ${messages.length} messages, ~${totalHistoryTokens} tokens`, + ); + } + + // Add current message + messages.push({ + role: "user", + content: message, + }); + + console.log( + `Starting stream for message: ${message} (history: ${messages.length - 1} messages)`, + ); + + // First pass - stream the response + const result = await agent.streamText({ messages }); + let { toolsCalled, toolResults, hasText } = await processStreamEvents( + result.fullStream, + res, + ); + + console.log("First pass complete. Tools called:", [...toolsCalled]); + + // Check if this looks like an organization analysis that needs more tool calls + const hasOrgDiscovery = toolsCalled.has("discover_organization"); + const hasAIServicesDiscovery = toolsCalled.has("discover_ai_services"); + + // Need AI services discovery if we have org but not AI services + const needsAIServicesDiscovery = hasOrgDiscovery && !hasAIServicesDiscovery; + + // If discover_ai_services wasn't called but discover_organization was, make a follow-up request for AI services + if (needsAIServicesDiscovery && !hasText) { + console.log( + "⚠️ discover_organization called but discover_ai_services missing. Making follow-up request...", + ); + + const orgContext = toolResults.get("discover_organization"); + + // List which tools were already called to prevent duplicates + const alreadyCalled = [...toolsCalled].join(", "); + + const aiServicesFollowUp = ` +You called discover_organization but SKIPPED discover_ai_services. + +## TOOLS ALREADY CALLED (DO NOT CALL AGAIN): ${alreadyCalled} + +## CRITICAL: Call discover_ai_services NOW (ONLY ONCE) + +Organization context is ready: +- Name: ${orgContext?.organization?.name || "Unknown"} +- Sector: ${orgContext?.organization?.sector || "Unknown"} + +Call discover_ai_services ONCE with: +- organizationContext: Use the organization profile from discover_organization +- systemNames: Extract any AI systems mentioned in the user's original query + +After discover_ai_services completes, call assess_compliance ONCE with BOTH contexts. + +⚠️ EACH TOOL MUST BE CALLED EXACTLY ONCE - NO DUPLICATES!`; + + const aiServicesMessages = [ + ...messages, + { + role: "assistant", + content: `I have gathered the organization profile for ${orgContext?.organization?.name || "the organization"}. Now I will discover their AI systems.`, + }, + { + role: "user", + content: aiServicesFollowUp, + }, + ]; + + console.log("Making follow-up request to call discover_ai_services..."); + + const aiServicesResult = await agent.streamText({ + messages: aiServicesMessages, + }); + const aiServicesData = await processStreamEvents( + aiServicesResult.fullStream, + res, + ); + + // Update tracking with follow-up results (only add new tools) + for (const [tool, result] of aiServicesData.toolResults) { + if (!toolResults.has(tool)) { + toolResults.set(tool, result); + } + } + for (const tool of aiServicesData.toolsCalled) { + toolsCalled.add(tool); + } + hasText = hasText || aiServicesData.hasText; + + // Update needsAssessment check + const nowHasAssessment = toolsCalled.has("assess_compliance"); + if (!nowHasAssessment) { + console.log( + "discover_ai_services called but assess_compliance still missing...", + ); + } + } + + // Recalculate if we still need assessment after AI services discovery + const stillNeedsAssessment = + (toolsCalled.has("discover_organization") || + toolsCalled.has("discover_ai_services")) && + !toolsCalled.has("assess_compliance"); + + // If organization/AI services tools were called but assess_compliance wasn't, make a follow-up request + if (stillNeedsAssessment && !hasText) { + console.log( + "⚠️ Organization/AI tools called but assess_compliance missing. Making follow-up request...", + ); + + // Build context from tool results - these are the FULL results from the previous tools + const orgContext = toolResults.get("discover_organization"); + const aiServicesContext = toolResults.get("discover_ai_services"); + + // Create a follow-up message that includes the COMPLETE tool results as JSON + // This ensures the model has all the data needed to call assess_compliance correctly + const alreadyCalledTools = [...toolsCalled].join(", "); + + const fullContextMessage = ` +I have received the complete results from the previous tools. Now I need you to call assess_compliance with the FULL context. + +## ⚠️ TOOLS ALREADY CALLED (DO NOT CALL AGAIN): ${alreadyCalledTools} + +## COMPLETE ORGANIZATION CONTEXT (from discover_organization): +\`\`\`json +${JSON.stringify(orgContext, null, 2)} +\`\`\` + +## COMPLETE AI SERVICES CONTEXT (from discover_ai_services): +\`\`\`json +${JSON.stringify(aiServicesContext, null, 2)} +\`\`\` + +## INSTRUCTION: +Call assess_compliance ONCE with these EXACT parameters: +- organizationContext: Pass the COMPLETE organization context JSON shown above (not a summary) +- aiServicesContext: Pass the COMPLETE AI services context JSON shown above (not a summary) +- generateDocumentation: true + +⚠️ CALL assess_compliance EXACTLY ONCE - DO NOT call any tool that was already called! +After assess_compliance returns, provide a human-readable summary of the compliance assessment.`; + + const followUpMessages = [ + ...messages, + { + role: "assistant", + content: `I have gathered the organization profile for ${orgContext?.organization?.name || "the organization"} and discovered ${aiServicesContext?.systems?.length || 0} AI systems. Now I will call assess_compliance with the complete context to generate the full compliance report.`, + }, + { + role: "user", + content: fullContextMessage, + }, + ]; + + console.log( + "Making follow-up request to call assess_compliance with FULL context...", + ); + console.log( + `Organization context size: ${JSON.stringify(orgContext || {}).length} chars`, + ); + console.log( + `AI services context size: ${JSON.stringify(aiServicesContext || {}).length} chars`, + ); + + const followUpResult = await agent.streamText({ + messages: followUpMessages, + }); + const followUpData = await processStreamEvents( + followUpResult.fullStream, + res, + ); + + // Update tracking with follow-up results + for (const [tool, result] of followUpData.toolResults) { + toolResults.set(tool, result); + } + for (const tool of followUpData.toolsCalled) { + toolsCalled.add(tool); + } + // Update hasText from follow-up + hasText = hasText || followUpData.hasText; + } + + // Final check for text + const hasTextNow = hasText; + + // If still no text response, generate a comprehensive summary based on available tool results + if (!hasTextNow && toolResults.size > 0) { + console.log( + "Generating comprehensive compliance report from tool results...", + ); + + // Create a summary from available data + const orgData = toolResults.get("discover_organization"); + const aiData = toolResults.get("discover_ai_services"); + const assessData = toolResults.get("assess_compliance"); + + let summary = "\n\n---\n\n"; + + // ================== HEADER ================== + const orgName = orgData?.organization?.name || "Organization"; + summary += `# 🇪🇺 EU AI Act Compliance Report\n`; + summary += `## ${orgName}\n\n`; + summary += `*Assessment Date: ${new Date().toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" })}*\n\n`; + summary += `---\n\n`; + + // ================== ORGANIZATION PROFILE ================== + if (orgData?.organization) { + const org = orgData.organization; + summary += `## 🏢 Organization Profile\n\n`; + summary += `| Attribute | Value |\n`; + summary += `|-----------|-------|\n`; + summary += `| **Name** | ${org.name} |\n`; + summary += `| **Sector** | ${org.sector} |\n`; + summary += `| **Size** | ${org.size} |\n`; + summary += `| **Headquarters** | ${org.headquarters?.city || "Unknown"}, ${org.headquarters?.country || "Unknown"} |\n`; + summary += `| **EU Presence** | ${org.euPresence ? "✅ Yes" : "❌ No"} |\n`; + summary += `| **AI Maturity Level** | ${org.aiMaturityLevel} |\n`; + summary += `| **Primary Role** | ${org.primaryRole} (per Article 3) |\n`; + summary += `| **Jurisdictions** | ${org.jurisdiction?.join(", ") || "Unknown"} |\n`; + if (org.contact?.website) { + summary += `| **Website** | ${org.contact.website} |\n`; + } + summary += `\n`; + + // Regulatory Context + if (orgData.regulatoryContext) { + const reg = orgData.regulatoryContext; + summary += `### 📋 Regulatory Context\n\n`; + summary += `- **Quality Management System (Article 17):** ${reg.hasQualityManagementSystem ? "✅ Implemented" : "⚠️ Not Implemented"}\n`; + summary += `- **Risk Management System (Article 9):** ${reg.hasRiskManagementSystem ? "✅ Implemented" : "⚠️ Not Implemented"}\n`; + if (reg.existingCertifications?.length > 0) { + summary += `- **Certifications:** ${reg.existingCertifications.join(", ")}\n`; + } + if (!org.euPresence) { + summary += `- **Authorized Representative (Article 22):** ${reg.hasAuthorizedRepresentative ? "✅ Appointed" : "⚠️ Required for non-EU entities"}\n`; + } + summary += `\n`; + } + } + + // ================== AI SYSTEMS ANALYSIS ================== + if (aiData?.systems && aiData.systems.length > 0) { + summary += `## 🤖 AI Systems Analysis\n\n`; + + // Risk Summary Table + const riskSummary = aiData.riskSummary; + summary += `### Risk Distribution\n\n`; + summary += `| Risk Category | Count | Status |\n`; + summary += `|---------------|-------|--------|\n`; + if (riskSummary.unacceptableRiskCount > 0) { + summary += `| 🔴 **Unacceptable Risk** | ${riskSummary.unacceptableRiskCount} | ⛔ PROHIBITED |\n`; + } + summary += `| 🟠 **High Risk** | ${riskSummary.highRiskCount} | Requires Conformity Assessment |\n`; + summary += `| 🟡 **Limited Risk** | ${riskSummary.limitedRiskCount} | Transparency Obligations |\n`; + summary += `| 🟢 **Minimal Risk** | ${riskSummary.minimalRiskCount} | No Specific Obligations |\n`; + summary += `| **Total** | ${riskSummary.totalCount} | |\n\n`; + + // Detailed System Analysis + summary += `### Detailed System Analysis\n\n`; + + for (const sys of aiData.systems) { + const riskEmoji = + sys.riskClassification.category === "High" + ? "🟠" + : sys.riskClassification.category === "Limited" + ? "🟡" + : sys.riskClassification.category === "Unacceptable" + ? "🔴" + : "🟢"; + + summary += `#### ${riskEmoji} ${sys.system.name}\n\n`; + summary += `**Risk Classification:** ${sys.riskClassification.category} Risk (Score: ${sys.riskClassification.riskScore}/100)\n\n`; + + // Purpose and Description + summary += `**Intended Purpose:** ${sys.system.intendedPurpose}\n\n`; + + // Classification Reasoning + if (sys.riskClassification.justification) { + summary += `**Classification Reasoning:**\n> ${sys.riskClassification.justification}\n\n`; + } + + // Annex III Category for High-Risk + if ( + sys.riskClassification.category === "High" && + sys.riskClassification.annexIIICategory + ) { + summary += `**Annex III Category:** ${sys.riskClassification.annexIIICategory}\n\n`; + } + + // Technical Details + summary += `**Technical Details:**\n`; + summary += `- AI Technology: ${sys.technicalDetails.aiTechnology?.join(", ") || "Not specified"}\n`; + summary += `- Data Processed: ${sys.technicalDetails.dataProcessed?.join(", ") || "Not specified"}\n`; + summary += `- Deployment: ${sys.technicalDetails.deploymentModel || "Not specified"}\n`; + summary += `- Human Oversight: ${sys.technicalDetails.humanOversight?.enabled ? "✅ Enabled" : "⚠️ Not enabled"}\n`; + if (sys.technicalDetails.humanOversight?.description) { + summary += ` - *${sys.technicalDetails.humanOversight.description}*\n`; + } + summary += `\n`; + + // Compliance Status + summary += `**Compliance Status:**\n`; + summary += `- Conformity Assessment: ${sys.complianceStatus.conformityAssessmentStatus}\n`; + summary += `- Technical Documentation: ${sys.complianceStatus.hasTechnicalDocumentation ? "✅" : "❌"}\n`; + summary += `- EU Database Registration: ${sys.complianceStatus.registeredInEUDatabase ? "✅" : "❌"}\n`; + summary += `- Post-Market Monitoring: ${sys.complianceStatus.hasPostMarketMonitoring ? "✅" : "❌"}\n`; + if (sys.complianceStatus.complianceDeadline) { + summary += `- **Deadline:** ${sys.complianceStatus.complianceDeadline}\n`; + } + if (sys.complianceStatus.estimatedComplianceEffort) { + summary += `- **Estimated Effort:** ${sys.complianceStatus.estimatedComplianceEffort}\n`; + } + summary += `\n`; + + // Regulatory References + if (sys.riskClassification.regulatoryReferences?.length > 0) { + summary += `**Applicable Articles:** ${sys.riskClassification.regulatoryReferences.join(", ")}\n\n`; + } + + summary += `---\n\n`; + } + } + + // ================== COMPLIANCE ASSESSMENT ================== + if (assessData?.assessment) { + const assess = assessData.assessment; + + summary += `## 📊 Compliance Assessment Results\n\n`; + + // Score Card + const scoreEmoji = + assess.overallScore >= 80 + ? "🟢" + : assess.overallScore >= 60 + ? "🟡" + : assess.overallScore >= 40 + ? "🟠" + : "🔴"; + summary += `### Overall Score: ${scoreEmoji} ${assess.overallScore}/100\n`; + summary += `**Risk Level:** ${assess.riskLevel}\n\n`; + + // Compliance by Article + if ( + assess.complianceByArticle && + Object.keys(assess.complianceByArticle).length > 0 + ) { + summary += `### Compliance by EU AI Act Article\n\n`; + summary += `| Article | Status | Issues |\n`; + summary += `|---------|--------|--------|\n`; + for (const [article, statusData] of Object.entries( + assess.complianceByArticle, + )) { + const articleStatus = statusData as { + compliant: boolean; + gaps?: string[]; + }; + const icon = articleStatus.compliant ? "✅" : "❌"; + const issues = articleStatus.gaps?.length + ? articleStatus.gaps.length + " gap(s)" + : "None"; + summary += `| ${article} | ${icon} | ${issues} |\n`; + } + summary += `\n`; + } + + // Gap Analysis + if (assess.gaps && assess.gaps.length > 0) { + summary += `### 🔍 Gap Analysis\n\n`; + + // Group by severity + const critical = assess.gaps.filter( + (g: any) => g.severity === "CRITICAL", + ); + const high = assess.gaps.filter((g: any) => g.severity === "HIGH"); + const medium = assess.gaps.filter( + (g: any) => g.severity === "MEDIUM", + ); + const low = assess.gaps.filter((g: any) => g.severity === "LOW"); + + if (critical.length > 0) { + summary += `#### 🔴 Critical Gaps (${critical.length})\n\n`; + for (const gap of critical) { + summary += `**${gap.category}** - ${gap.articleReference || "General"}\n`; + summary += `> ${gap.description}\n`; + if (gap.currentState) + summary += `> *Current:* ${gap.currentState}\n`; + if (gap.requiredState) + summary += `> *Required:* ${gap.requiredState}\n`; + if (gap.deadline) summary += `> ⏰ Deadline: ${gap.deadline}\n`; + summary += `\n`; + } + } + + if (high.length > 0) { + summary += `#### 🟠 High Priority Gaps (${high.length})\n\n`; + for (const gap of high) { + summary += `**${gap.category}** - ${gap.articleReference || "General"}\n`; + summary += `> ${gap.description}\n`; + if (gap.deadline) summary += `> ⏰ Deadline: ${gap.deadline}\n`; + summary += `\n`; + } + } + + if (medium.length > 0) { + summary += `#### 🟡 Medium Priority Gaps (${medium.length})\n\n`; + for (const gap of medium.slice(0, 5)) { + summary += `- **${gap.category}:** ${gap.description}\n`; + } + if (medium.length > 5) { + summary += `- *...and ${medium.length - 5} more medium-priority gaps*\n`; + } + summary += `\n`; + } + + if (low.length > 0) { + summary += `#### 🟢 Low Priority Gaps (${low.length})\n\n`; + summary += `*${low.length} low-priority gaps identified - see detailed report*\n\n`; + } + } + + // Recommendations + if (assess.recommendations && assess.recommendations.length > 0) { + summary += `### 💡 Priority Recommendations\n\n`; + + // Sort by priority + const sortedRecs = [...assess.recommendations].sort( + (a: any, b: any) => a.priority - b.priority, + ); + + for (const rec of sortedRecs.slice(0, 5)) { + summary += `#### ${rec.priority}. ${rec.title}\n`; + summary += `*${rec.articleReference || "General Compliance"}*\n\n`; + summary += `${rec.description}\n\n`; + + if (rec.implementationSteps && rec.implementationSteps.length > 0) { + summary += `**Implementation Steps:**\n`; + for ( + let i = 0; + i < Math.min(rec.implementationSteps.length, 5); + i++ + ) { + summary += `${i + 1}. ${rec.implementationSteps[i]}\n`; + } + summary += `\n`; + } + + if (rec.estimatedEffort) { + summary += `**Estimated Effort:** ${rec.estimatedEffort}\n`; + } + if (rec.expectedOutcome) { + summary += `**Expected Outcome:** ${rec.expectedOutcome}\n`; + } + summary += `\n`; + } + + if (sortedRecs.length > 5) { + summary += `*...and ${sortedRecs.length - 5} additional recommendations*\n\n`; + } + } + } + + // ================== KEY COMPLIANCE DEADLINES ================== + if (aiData?.complianceDeadlines) { + summary += `## 📅 Key Compliance Deadlines\n\n`; + summary += `| Deadline | Requirement |\n`; + summary += `|----------|-------------|\n`; + summary += `| **February 2, 2025** | Prohibited AI practices ban (Article 5) |\n`; + summary += `| **August 2, 2025** | GPAI model obligations (Article 53) |\n`; + summary += `| **${aiData.complianceDeadlines.limitedRisk}** | Limited-risk transparency (Article 50) |\n`; + summary += `| **${aiData.complianceDeadlines.highRisk}** | High-risk AI full compliance |\n`; + summary += `\n`; + } + + // ================== DOCUMENTATION TEMPLATES ================== + if (assessData?.documentation) { + const docs = assessData.documentation; + summary += `## 📝 Generated Documentation Templates\n\n`; + summary += `The following EU AI Act compliance documentation templates have been generated:\n\n`; + + const docList = [ + { + name: "Risk Management System", + field: "riskManagementTemplate", + article: "Article 9", + }, + { + name: "Technical Documentation", + field: "technicalDocumentation", + article: "Article 11, Annex IV", + }, + { + name: "Conformity Assessment", + field: "conformityAssessment", + article: "Article 43", + }, + { + name: "Transparency Notice", + field: "transparencyNotice", + article: "Article 50", + }, + { + name: "Quality Management System", + field: "qualityManagementSystem", + article: "Article 17", + }, + { + name: "Human Oversight Procedure", + field: "humanOversightProcedure", + article: "Article 14", + }, + { + name: "Data Governance Policy", + field: "dataGovernancePolicy", + article: "Article 10", + }, + { + name: "Incident Reporting Procedure", + field: "incidentReportingProcedure", + article: "General", + }, + ]; + + summary += `| Document | Article Reference | Status |\n`; + summary += `|----------|-------------------|--------|\n`; + for (const doc of docList) { + const hasDoc = (docs as any)[doc.field]; + summary += `| ${doc.name} | ${doc.article} | ${hasDoc ? "✅ Generated" : "⚪ Not generated"} |\n`; + } + summary += `\n`; + + // Show first template as example + const firstTemplate = + docs.riskManagementTemplate || + docs.technicalDocumentation || + docs.transparencyNotice; + if (firstTemplate) { + summary += `### 📄 Sample Template: Risk Management System (Article 9)\n\n`; + summary += `
\nClick to expand template\n\n`; + summary += `${firstTemplate.substring(0, 2000)}${firstTemplate.length > 2000 ? "\n\n*...template truncated for display...*" : ""}\n`; + summary += `\n
\n\n`; + } + } + + // ================== AI REASONING ================== + if (assessData?.reasoning) { + summary += `## 🧠 Assessment Reasoning\n\n`; + summary += `
\nClick to expand AI analysis reasoning\n\n`; + summary += `${assessData.reasoning}\n`; + summary += `\n
\n\n`; + } + + summary += `---\n\n`; + summary += `*Report generated on ${new Date().toISOString()}*\n\n`; + summary += `**Disclaimer:** This report is for informational purposes only and does not constitute legal advice. Consult with qualified legal professionals for official compliance guidance.\n`; + + // Stream the comprehensive summary + for (const char of summary) { + res.write( + `data: ${JSON.stringify({ type: "text", content: char })}\n\n`, + ); + } + } + + // Send final done message + res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`); + res.end(); + } catch (error) { + console.error("Chat error:", error); + + // Try to send error via stream if headers already sent + if (res.headersSent) { + res.write( + `data: ${JSON.stringify({ + type: "error", + error: error instanceof Error ? error.message : "Unknown error", + })}\n\n`, + ); + res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`); + res.end(); + } else { + res.status(500).json({ + error: "Internal server error", + message: error instanceof Error ? error.message : "Unknown error", + }); + } + } +}); + +// Tool status endpoint +app.get("/api/tools", async (_req, res) => { + try { + // Use default GPT-OSS (free, no API key needed) just to list tools + const agent = createAgent({ + modelName: "gpt-oss", + apiKeys: { + modalEndpointUrl: + "https://vasilis--gpt-oss-vllm-inference-serve.modal.run", + }, + }); + const tools = await agent.getTools(); + + res.json({ + tools: tools.map((tool: any) => ({ + name: tool.name, + description: tool.description, + })), + }); + } catch (error) { + console.error("Tools error:", error); + res.status(500).json({ error: "Failed to fetch tools" }); + } +}); + +// ============================================================================ +// DIRECT TOOL ENDPOINTS - For ChatGPT Apps and direct API calls +// ============================================================================ + +/** + * Direct endpoint for discover_organization tool + * Used by ChatGPT Apps via Gradio MCP server + */ +app.post("/api/tools/discover_organization", async (req, res) => { + try { + const { organizationName, domain, context } = req.body; + + if (!organizationName) { + return res.status(400).json({ error: "organizationName is required" }); + } + + console.log(`[API] discover_organization called for: ${organizationName}`); + + // Read API keys from headers (from Gradio UI), fallback to server env (HF Spaces secret) + const tavilyApiKey = + (req.headers["x-tavily-api-key"] as string) || + process.env.TAVILY_API_KEY || + undefined; + if (tavilyApiKey) { + console.log( + `[API] Using Tavily API key from: ${req.headers["x-tavily-api-key"] ? "request header" : "server env (HF Spaces secret)"}`, + ); + } else { + console.log(`[API] No Tavily API key - will use AI model fallback`); + } + const modelName = (req.headers["x-ai-model"] as string) || "gpt-oss"; + const apiKeys = { + modalEndpointUrl: + (req.headers["x-modal-endpoint-url"] as string) || undefined, + openaiApiKey: (req.headers["x-openai-api-key"] as string) || undefined, + xaiApiKey: (req.headers["x-xai-api-key"] as string) || undefined, + anthropicApiKey: + (req.headers["x-anthropic-api-key"] as string) || undefined, + googleApiKey: (req.headers["x-google-api-key"] as string) || undefined, + }; + + const result = await discoverOrganization({ + organizationName, + domain: domain || undefined, + context: context || undefined, + model: modelName, + apiKeys, + tavilyApiKey, + }); + + console.log( + `[API] discover_organization completed for: ${organizationName}`, + ); + res.json(result); + } catch (error) { + console.error("discover_organization error:", error); + res.status(500).json({ + error: true, + message: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * Direct endpoint for discover_ai_services tool + * Used by ChatGPT Apps via Gradio MCP server + */ +app.post("/api/tools/discover_ai_services", async (req, res) => { + try { + const { organizationContext, systemNames, scope, context } = req.body; + + console.log( + `[API] discover_ai_services called, systemNames: ${JSON.stringify(systemNames)}`, + ); + + // Read API keys from headers (from Gradio UI), fallback to server env (HF Spaces secret) + const tavilyApiKey = + (req.headers["x-tavily-api-key"] as string) || + process.env.TAVILY_API_KEY || + undefined; + if (tavilyApiKey) { + console.log( + `[API] Using Tavily API key from: ${req.headers["x-tavily-api-key"] ? "request header" : "server env (HF Spaces secret)"}`, + ); + } else { + console.log(`[API] No Tavily API key - will use AI model fallback`); + } + const modelName = (req.headers["x-ai-model"] as string) || "gpt-oss"; + const apiKeys = { + modalEndpointUrl: + (req.headers["x-modal-endpoint-url"] as string) || undefined, + openaiApiKey: (req.headers["x-openai-api-key"] as string) || undefined, + xaiApiKey: (req.headers["x-xai-api-key"] as string) || undefined, + anthropicApiKey: + (req.headers["x-anthropic-api-key"] as string) || undefined, + googleApiKey: (req.headers["x-google-api-key"] as string) || undefined, + }; + + const result = await discoverAIServices({ + organizationContext: organizationContext || undefined, + systemNames: systemNames || undefined, + scope: scope || undefined, + context: context || undefined, + model: modelName, + apiKeys, + tavilyApiKey, + }); + + console.log( + `[API] discover_ai_services completed, found ${result.systems?.length || 0} systems`, + ); + res.json(result); + } catch (error) { + console.error("discover_ai_services error:", error); + res.status(500).json({ + error: true, + message: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +/** + * Direct endpoint for assess_compliance tool + * Used by ChatGPT Apps via Gradio MCP server + * + * Note: This endpoint sets env vars for the MCP tool to read. + * The main /api/chat endpoint uses direct API key passing instead. + */ +app.post("/api/tools/assess_compliance", async (req, res) => { + try { + const { + organizationContext, + aiServicesContext, + focusAreas, + generateDocumentation, + } = req.body; + + console.log( + `[API] assess_compliance called, generateDocumentation: ${generateDocumentation}`, + ); + + // Read model selection and API keys from headers (from Gradio UI), fallback to server env (HF Spaces secret) + const modelName = (req.headers["x-ai-model"] as string) || "gpt-oss"; + const tavilyApiKey = + (req.headers["x-tavily-api-key"] as string) || + process.env.TAVILY_API_KEY || + undefined; + if (tavilyApiKey) { + console.log( + `[API] Using Tavily API key from: ${req.headers["x-tavily-api-key"] ? "request header" : "server env (HF Spaces secret)"}`, + ); + } else { + console.log(`[API] No Tavily API key - will use AI model fallback`); + } + const apiKeys = { + modalEndpointUrl: + (req.headers["x-modal-endpoint-url"] as string) || undefined, + openaiApiKey: (req.headers["x-openai-api-key"] as string) || undefined, + xaiApiKey: (req.headers["x-xai-api-key"] as string) || undefined, + anthropicApiKey: + (req.headers["x-anthropic-api-key"] as string) || undefined, + googleApiKey: (req.headers["x-google-api-key"] as string) || undefined, + }; + + // For GPT-OSS, use default Modal endpoint if not provided + if (modelName === "gpt-oss" && !apiKeys.modalEndpointUrl) { + apiKeys.modalEndpointUrl = + "https://vasilis--gpt-oss-vllm-inference-serve.modal.run"; + } + + const result = await assessCompliance({ + organizationContext: organizationContext || undefined, + aiServicesContext: aiServicesContext || undefined, + focusAreas: focusAreas || undefined, + generateDocumentation: generateDocumentation !== false, // Default true + model: modelName, + apiKeys, + tavilyApiKey, + }); + + console.log( + `[API] assess_compliance completed, score: ${result.assessment?.overallScore}`, + ); + res.json(result); + } catch (error) { + console.error("assess_compliance error:", error); + res.status(500).json({ + error: true, + message: error instanceof Error ? error.message : "Unknown error", + }); + } +}); + +// Start server +app.listen(PORT, () => { + const PUBLIC_URL = process.env.PUBLIC_URL; + const isProduction = process.env.NODE_ENV === "production"; + + console.log(`\n🇪🇺 EU AI Act Compliance Agent Server`); + console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`); + + if (isProduction) { + console.log(`🌐 Environment: PRODUCTION (HF Spaces)`); + console.log(`✓ Gradio UI: ${PUBLIC_URL || "https://*.hf.space"}`); + console.log(`✓ API Server: http://localhost:${PORT} (internal only)`); + console.log(`\n📡 Internal API Endpoints (used by Gradio):`); + } else { + console.log(`🛠️ Environment: LOCAL DEVELOPMENT`); + console.log(`✓ Server running on http://localhost:${PORT}`); + console.log(`\n📡 API Endpoints:`); + } + + console.log(` • GET /health`); + console.log(` • POST /api/chat`); + console.log(` • GET /api/tools`); + console.log(` • POST /api/tools/discover_organization`); + console.log(` • POST /api/tools/discover_ai_services`); + console.log(` • POST /api/tools/assess_compliance`); + + if (!isProduction) { + console.log(`\n💡 Start Gradio UI: pnpm gradio`); + console.log(`💡 Start ChatGPT App: pnpm chatgpt-app`); + } + console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`); +}); diff --git a/apps/eu-ai-act-agent/src/types/index.ts b/apps/eu-ai-act-agent/src/types/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fbbdd196d181f40779b3a3c0e0f0565ebd286688 --- /dev/null +++ b/apps/eu-ai-act-agent/src/types/index.ts @@ -0,0 +1,43 @@ +/** + * Type definitions for EU AI Act Compliance Agent + */ + +export interface ChatMessage { + role: "user" | "assistant" | "system"; + content: string; +} + +export interface ChatRequest { + message: string; + history: ChatMessage[]; +} + +export interface ChatResponse { + type: "text" | "tool_call" | "result" | "done" | "error"; + content?: string; + tool?: string; + data?: any; + error?: string; +} + +export interface ToolDefinition { + name: string; + description: string; + parameters: Record; +} + +export interface AgentConfig { + model: string; + temperature?: number; + maxTokens?: number; + maxSteps?: number; +} + +// Re-export types from MCP package +// @ts-ignore - These will be available at runtime after building +export type { + OrganizationProfile, + AIServiceDiscovery, + ComplianceAssessment, +} from "../../../eu-ai-act-mcp/dist/types/index.js"; + diff --git a/apps/eu-ai-act-agent/start.sh b/apps/eu-ai-act-agent/start.sh new file mode 100755 index 0000000000000000000000000000000000000000..f7f3f524217e95dc5c8c33ed09ebc9a86753bb4b --- /dev/null +++ b/apps/eu-ai-act-agent/start.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +# EU AI Act Compliance Agent Startup Script +# Starts both the API server and Gradio UI + +set -e + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "🇪🇺 EU AI Act Compliance Agent" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +# Check if .env exists +if [ ! -f "../../.env" ]; then + echo "⚠️ Warning: .env file not found" + echo " Create one from .env.example and add your OPENAI_API_KEY" + echo "" +fi + +# Check Node.js +if ! command -v node &> /dev/null; then + echo "❌ Node.js not found. Please install Node.js 18+" + exit 1 +fi + +# Check Python +if ! command -v python3 &> /dev/null; then + echo "❌ Python 3 not found. Please install Python 3.9+" + exit 1 +fi + +echo "✓ Node.js: $(node --version)" +echo "✓ Python: $(python3 --version)" +echo "" + +# Check if uv is installed +if ! command -v uv &> /dev/null; then + echo "📦 Installing uv (fast Python package manager)..." + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "" + echo "⚠️ Please restart your terminal and run this script again" + exit 0 +fi + +# Create virtual environment if it doesn't exist +if [ ! -d ".venv" ]; then + echo "📦 Creating virtual environment with Python 3.13..." + uv venv --python python3.13 + echo "" +fi + +# Activate virtual environment +source .venv/bin/activate + +# Install Python dependencies if needed +if ! python -c "import gradio" 2>/dev/null; then + echo "📦 Installing Python dependencies with uv..." + uv pip install -r requirements.txt + echo "" +fi + +# Build MCP server if needed +if [ ! -d "../../packages/eu-ai-act-mcp/dist" ]; then + echo "🔨 Building MCP server..." + cd ../../ + pnpm --filter @eu-ai-act/mcp-server build + cd apps/eu-ai-act-agent + echo "" +fi + +echo "🚀 Starting EU AI Act Compliance Agent..." +echo "" +echo "📡 API Server will start on: http://localhost:3001" +echo "🎨 Gradio UI will start on: http://localhost:7860" +echo "" +echo "Press Ctrl+C to stop both servers" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +# Function to cleanup on exit +cleanup() { + echo "" + echo "🛑 Shutting down servers..." + kill $API_PID $GRADIO_PID $CHATGPT_PID 2>/dev/null + exit 0 +} + +trap cleanup INT TERM + +# Start API server in background +echo "Starting API server..." +pnpm dev > /tmp/eu-ai-act-api.log 2>&1 & +API_PID=$! + +# Wait for API to be ready +echo "Waiting for API server to start..." +sleep 3 + +# Start Gradio UI in background (using virtual environment Python) +echo "Starting Gradio UI..." +python src/gradio_app.py > /tmp/eu-ai-act-gradio.log 2>&1 & +GRADIO_PID=$! + +# Start ChatGPT App in background (using virtual environment Python) +echo "Starting ChatGPT App..." +python src/chatgpt_app.py > /tmp/eu-ai-act-chatgpt.log 2>&1 & +CHATGPT_PID=$! + +# Wait for apps to be ready +sleep 3 + +echo "" +echo "✅ All servers are running!" +echo "" +echo "🌐 Open your browser to:" +echo " Gradio UI: http://localhost:7860" +echo " ChatGPT App: http://localhost:7861" +echo "" +echo "📋 Logs:" +echo " API: tail -f /tmp/eu-ai-act-api.log" +echo " Gradio: tail -f /tmp/eu-ai-act-gradio.log" +echo " ChatGPT App: tail -f /tmp/eu-ai-act-chatgpt.log" +echo "" + +# Wait for all processes +wait $API_PID $GRADIO_PID $CHATGPT_PID + diff --git a/apps/eu-ai-act-agent/tsconfig.json b/apps/eu-ai-act-agent/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..bc0d3c1fbdf8256facc00b9c2843f9c3ea9c2dc0 --- /dev/null +++ b/apps/eu-ai-act-agent/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tooling/typescript/base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "lib": ["ES2022"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/apps/eu-ai-act-agent/tsup.config.ts b/apps/eu-ai-act-agent/tsup.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce4e130dd2a56f39ed17e1931045f12114a46c9a --- /dev/null +++ b/apps/eu-ai-act-agent/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/server.ts"], + format: ["esm"], + dts: false, // Disable dts generation to avoid module resolution issues + sourcemap: true, + clean: true, + minify: false, + external: ["express", "dotenv"], + noExternal: [], // Bundle everything except external + bundle: true, +}); + diff --git a/apps/eu-ai-act-agent/tsx b/apps/eu-ai-act-agent/tsx new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/eu-ai-act-agent/uv.lock b/apps/eu-ai-act-agent/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..545e26756642407a52e7abf874883eee773f98b9 --- /dev/null +++ b/apps/eu-ai-act-agent/uv.lock @@ -0,0 +1,1905 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", + "python_full_version < '3.11'", +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896 }, +] + +[[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 = "anyio" +version = "4.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097 }, +] + +[[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 = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523 }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455 }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997 }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844 }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056 }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892 }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660 }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143 }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313 }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044 }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766 }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640 }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052 }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185 }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503 }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173 }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096 }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748 }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329 }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407 }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811 }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470 }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878 }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867 }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001 }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046 }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788 }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472 }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279 }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568 }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942 }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603 }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104 }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754 }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332 }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396 }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811 }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483 }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885 }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899 }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998 }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046 }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843 }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490 }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297 }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331 }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697 }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206 }, +] + +[[package]] +name = "brotli" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/10/a090475284fc4a71aed40a96f32e44a7fe5bda39687353dd977720b211b6/brotli-1.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3b90b767916ac44e93a8e28ce6adf8d551e43affb512f2377c732d486ac6514e", size = 863089 }, + { url = "https://files.pythonhosted.org/packages/03/41/17416630e46c07ac21e378c3464815dd2e120b441e641bc516ac32cc51d2/brotli-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6be67c19e0b0c56365c6a76e393b932fb0e78b3b56b711d180dd7013cb1fd984", size = 445442 }, + { url = "https://files.pythonhosted.org/packages/24/31/90cc06584deb5d4fcafc0985e37741fc6b9717926a78674bbb3ce018957e/brotli-1.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0bbd5b5ccd157ae7913750476d48099aaf507a79841c0d04a9db4415b14842de", size = 1532658 }, + { url = "https://files.pythonhosted.org/packages/62/17/33bf0c83bcbc96756dfd712201d87342732fad70bb3472c27e833a44a4f9/brotli-1.2.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3f3c908bcc404c90c77d5a073e55271a0a498f4e0756e48127c35d91cf155947", size = 1631241 }, + { url = "https://files.pythonhosted.org/packages/48/10/f47854a1917b62efe29bc98ac18e5d4f71df03f629184575b862ef2e743b/brotli-1.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b557b29782a643420e08d75aea889462a4a8796e9a6cf5621ab05a3f7da8ef2", size = 1424307 }, + { url = "https://files.pythonhosted.org/packages/e4/b7/f88eb461719259c17483484ea8456925ee057897f8e64487d76e24e5e38d/brotli-1.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:81da1b229b1889f25adadc929aeb9dbc4e922bd18561b65b08dd9343cfccca84", size = 1488208 }, + { url = "https://files.pythonhosted.org/packages/26/59/41bbcb983a0c48b0b8004203e74706c6b6e99a04f3c7ca6f4f41f364db50/brotli-1.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ff09cd8c5eec3b9d02d2408db41be150d8891c5566addce57513bf546e3d6c6d", size = 1597574 }, + { url = "https://files.pythonhosted.org/packages/8e/e6/8c89c3bdabbe802febb4c5c6ca224a395e97913b5df0dff11b54f23c1788/brotli-1.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a1778532b978d2536e79c05dac2d8cd857f6c55cd0c95ace5b03740824e0e2f1", size = 1492109 }, + { url = "https://files.pythonhosted.org/packages/ed/9a/4b19d4310b2dbd545c0c33f176b0528fa68c3cd0754e34b2f2bcf56548ae/brotli-1.2.0-cp310-cp310-win32.whl", hash = "sha256:b232029d100d393ae3c603c8ffd7e3fe6f798c5e28ddca5feabb8e8fdb732997", size = 334461 }, + { url = "https://files.pythonhosted.org/packages/ac/39/70981d9f47705e3c2b95c0847dfa3e7a37aa3b7c6030aedc4873081ed005/brotli-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:ef87b8ab2704da227e83a246356a2b179ef826f550f794b2c52cddb4efbd0196", size = 369035 }, + { url = "https://files.pythonhosted.org/packages/7a/ef/f285668811a9e1ddb47a18cb0b437d5fc2760d537a2fe8a57875ad6f8448/brotli-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:15b33fe93cedc4caaff8a0bd1eb7e3dab1c61bb22a0bf5bdfdfd97cd7da79744", size = 863110 }, + { url = "https://files.pythonhosted.org/packages/50/62/a3b77593587010c789a9d6eaa527c79e0848b7b860402cc64bc0bc28a86c/brotli-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:898be2be399c221d2671d29eed26b6b2713a02c2119168ed914e7d00ceadb56f", size = 445438 }, + { url = "https://files.pythonhosted.org/packages/cd/e1/7fadd47f40ce5549dc44493877db40292277db373da5053aff181656e16e/brotli-1.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350c8348f0e76fff0a0fd6c26755d2653863279d086d3aa2c290a6a7251135dd", size = 1534420 }, + { url = "https://files.pythonhosted.org/packages/12/8b/1ed2f64054a5a008a4ccd2f271dbba7a5fb1a3067a99f5ceadedd4c1d5a7/brotli-1.2.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e1ad3fda65ae0d93fec742a128d72e145c9c7a99ee2fcd667785d99eb25a7fe", size = 1632619 }, + { url = "https://files.pythonhosted.org/packages/89/5a/7071a621eb2d052d64efd5da2ef55ecdac7c3b0c6e4f9d519e9c66d987ef/brotli-1.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:40d918bce2b427a0c4ba189df7a006ac0c7277c180aee4617d99e9ccaaf59e6a", size = 1426014 }, + { url = "https://files.pythonhosted.org/packages/26/6d/0971a8ea435af5156acaaccec1a505f981c9c80227633851f2810abd252a/brotli-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2a7f1d03727130fc875448b65b127a9ec5d06d19d0148e7554384229706f9d1b", size = 1489661 }, + { url = "https://files.pythonhosted.org/packages/f3/75/c1baca8b4ec6c96a03ef8230fab2a785e35297632f402ebb1e78a1e39116/brotli-1.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9c79f57faa25d97900bfb119480806d783fba83cd09ee0b33c17623935b05fa3", size = 1599150 }, + { url = "https://files.pythonhosted.org/packages/0d/1a/23fcfee1c324fd48a63d7ebf4bac3a4115bdb1b00e600f80f727d850b1ae/brotli-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:844a8ceb8483fefafc412f85c14f2aae2fb69567bf2a0de53cdb88b73e7c43ae", size = 1493505 }, + { url = "https://files.pythonhosted.org/packages/36/e5/12904bbd36afeef53d45a84881a4810ae8810ad7e328a971ebbfd760a0b3/brotli-1.2.0-cp311-cp311-win32.whl", hash = "sha256:aa47441fa3026543513139cb8926a92a8e305ee9c71a6209ef7a97d91640ea03", size = 334451 }, + { url = "https://files.pythonhosted.org/packages/02/8b/ecb5761b989629a4758c394b9301607a5880de61ee2ee5fe104b87149ebc/brotli-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:022426c9e99fd65d9475dce5c195526f04bb8be8907607e27e747893f6ee3e24", size = 369035 }, + { url = "https://files.pythonhosted.org/packages/11/ee/b0a11ab2315c69bb9b45a2aaed022499c9c24a205c3a49c3513b541a7967/brotli-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:35d382625778834a7f3061b15423919aa03e4f5da34ac8e02c074e4b75ab4f84", size = 861543 }, + { url = "https://files.pythonhosted.org/packages/e1/2f/29c1459513cd35828e25531ebfcbf3e92a5e49f560b1777a9af7203eb46e/brotli-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a61c06b334bd99bc5ae84f1eeb36bfe01400264b3c352f968c6e30a10f9d08b", size = 444288 }, + { url = "https://files.pythonhosted.org/packages/3d/6f/feba03130d5fceadfa3a1bb102cb14650798c848b1df2a808356f939bb16/brotli-1.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:acec55bb7c90f1dfc476126f9711a8e81c9af7fb617409a9ee2953115343f08d", size = 1528071 }, + { url = "https://files.pythonhosted.org/packages/2b/38/f3abb554eee089bd15471057ba85f47e53a44a462cfce265d9bf7088eb09/brotli-1.2.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:260d3692396e1895c5034f204f0db022c056f9e2ac841593a4cf9426e2a3faca", size = 1626913 }, + { url = "https://files.pythonhosted.org/packages/03/a7/03aa61fbc3c5cbf99b44d158665f9b0dd3d8059be16c460208d9e385c837/brotli-1.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:072e7624b1fc4d601036ab3f4f27942ef772887e876beff0301d261210bca97f", size = 1419762 }, + { url = "https://files.pythonhosted.org/packages/21/1b/0374a89ee27d152a5069c356c96b93afd1b94eae83f1e004b57eb6ce2f10/brotli-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adedc4a67e15327dfdd04884873c6d5a01d3e3b6f61406f99b1ed4865a2f6d28", size = 1484494 }, + { url = "https://files.pythonhosted.org/packages/cf/57/69d4fe84a67aef4f524dcd075c6eee868d7850e85bf01d778a857d8dbe0a/brotli-1.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7a47ce5c2288702e09dc22a44d0ee6152f2c7eda97b3c8482d826a1f3cfc7da7", size = 1593302 }, + { url = "https://files.pythonhosted.org/packages/d5/3b/39e13ce78a8e9a621c5df3aeb5fd181fcc8caba8c48a194cd629771f6828/brotli-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:af43b8711a8264bb4e7d6d9a6d004c3a2019c04c01127a868709ec29962b6036", size = 1487913 }, + { url = "https://files.pythonhosted.org/packages/62/28/4d00cb9bd76a6357a66fcd54b4b6d70288385584063f4b07884c1e7286ac/brotli-1.2.0-cp312-cp312-win32.whl", hash = "sha256:e99befa0b48f3cd293dafeacdd0d191804d105d279e0b387a32054c1180f3161", size = 334362 }, + { url = "https://files.pythonhosted.org/packages/1c/4e/bc1dcac9498859d5e353c9b153627a3752868a9d5f05ce8dedd81a2354ab/brotli-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:b35c13ce241abdd44cb8ca70683f20c0c079728a36a996297adb5334adfc1c44", size = 369115 }, + { url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523 }, + { url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289 }, + { url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076 }, + { url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880 }, + { url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737 }, + { url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440 }, + { url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313 }, + { url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945 }, + { url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368 }, + { url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116 }, + { url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080 }, + { url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453 }, + { url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168 }, + { url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098 }, + { url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861 }, + { url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594 }, + { url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455 }, + { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164 }, + { url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280 }, + { url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639 }, +] + +[[package]] +name = "certifi" +version = "2025.11.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/8c/58f469717fa48465e4a50c014a0400602d3c437d7c0c468e17ada824da3a/certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316", size = 160538 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/7d/9bc192684cea499815ff478dfcdc13835ddf401365057044fb721ec6bddb/certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b", size = 159438 }, +] + +[[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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283 }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504 }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811 }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402 }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217 }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079 }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475 }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829 }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211 }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036 }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184 }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790 }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344 }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560 }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613 }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476 }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374 }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597 }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574 }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971 }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972 }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078 }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076 }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820 }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635 }, + { 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/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709 }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814 }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467 }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280 }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454 }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609 }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586 }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290 }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663 }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964 }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064 }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792 }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198 }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262 }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988 }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324 }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742 }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863 }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837 }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550 }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162 }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019 }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310 }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022 }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098 }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991 }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456 }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978 }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969 }, + { 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 = "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'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +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 }, + { url = "https://files.pythonhosted.org/packages/d9/cd/1a8633802d766a0fa46f382a77e096d7e209e0817892929655fe0586ae32/cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32", size = 3689163 }, + { url = "https://files.pythonhosted.org/packages/4c/59/6b26512964ace6480c3e54681a9859c974172fb141c38df11eadd8416947/cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c", size = 3429474 }, + { url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132 }, + { url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992 }, + { url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944 }, + { url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957 }, + { url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447 }, + { url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528 }, +] + +[[package]] +name = "eu-ai-act-agent" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "gradio", extra = ["mcp"] }, + { name = "python-dotenv" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "gradio", extras = ["mcp"], specifier = ">=6.0.0" }, + { name = "python-dotenv", specifier = ">=1.0.0" }, + { name = "requests", specifier = ">=2.31.0" }, +] + +[package.metadata.requires-dev] +dev = [] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + +[[package]] +name = "fastapi" +version = "0.122.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/b2/de/3ee97a4f6ffef1fb70bf20561e4f88531633bb5045dc6cebc0f8471f764d/fastapi-0.122.0.tar.gz", hash = "sha256:cd9b5352031f93773228af8b4c443eedc2ac2aa74b27780387b853c3726fb94b", size = 346436 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/93/aa8072af4ff37b795f6bbf43dcaf61115f40f49935c7dbb180c9afc3f421/fastapi-0.122.0-py3-none-any.whl", hash = "sha256:a456e8915dfc6c8914a50d9651133bd47ec96d331c5b44600baa635538a30d67", size = 110671 }, +] + +[[package]] +name = "ffmpy" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/d2/1c4c582d71bcc65c76fa69fab85de6257d50fdf6fd4a2317c53917e9a581/ffmpy-1.0.0.tar.gz", hash = "sha256:b12932e95435c8820f1cd041024402765f821971e4bae753b327fc02a6e12f8b", size = 5101 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/56/dd3669eccebb6d8ac81e624542ebd53fe6f08e1b8f2f8d50aeb7e3b83f99/ffmpy-1.0.0-py3-none-any.whl", hash = "sha256:5640e5f0fd03fb6236d0e119b16ccf6522db1c826fdf35dcb87087b60fd7504f", size = 5614 }, +] + +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054 }, +] + +[[package]] +name = "fsspec" +version = "2025.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966 }, +] + +[[package]] +name = "gradio" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "brotli" }, + { name = "fastapi" }, + { name = "ffmpy" }, + { name = "gradio-client" }, + { name = "groovy" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydub" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "safehttpx" }, + { name = "semantic-version" }, + { name = "starlette" }, + { name = "tomlkit" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/13/f2bfe1237b8700f63e21c5e39f2843ac8346f7ba4525b582f30f40249863/gradio-6.0.1.tar.gz", hash = "sha256:5d02e6ac34c67aea26b938b8628c8f9f504871392e71f2db559ab8d6799bdf69", size = 36440914 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/21/27ae5f4b2191a5d58707fc610e67453781a2b948a675a7cf06c99497ffa1/gradio-6.0.1-py3-none-any.whl", hash = "sha256:0f98dc8b414a3f3773cbf3caf5a354507c8ae309ed8266e2f30ca9fa53f379b8", size = 21559963 }, +] + +[package.optional-dependencies] +mcp = [ + { name = "mcp" }, + { name = "pydantic" }, +] + +[[package]] +name = "gradio-client" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "packaging" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/0a/906062fe0577c62ea6e14044ba74268ff9266fdc75d0e69257bddb7400b3/gradio_client-2.0.0.tar.gz", hash = "sha256:56b462183cb8741bd3e69b21db7d3b62c5abb03c2c2bb925223f1eb18f950e89", size = 315906 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/5b/789403564754f1eba0273400c1cea2c155f984d82458279154977a088509/gradio_client-2.0.0-py3-none-any.whl", hash = "sha256:77bedf20edcc232d8e7986c1a22165b2bbca1c7c7df10ba808a093d5180dae18", size = 315180 }, +] + +[[package]] +name = "groovy" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090 }, +] + +[[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 = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870 }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584 }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004 }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636 }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448 }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401 }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866 }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861 }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699 }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885 }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550 }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010 }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264 }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071 }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099 }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178 }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214 }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054 }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812 }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920 }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735 }, +] + +[[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 = "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]] +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 = "huggingface-hub" +version = "1.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "shellingham" }, + { name = "tqdm" }, + { name = "typer-slim" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/02/c3d534d7498ba2792da1d2ce56b5d38bbcbcbbba62071c90ee289b408e8d/huggingface_hub-1.1.5.tar.gz", hash = "sha256:40ba5c9a08792d888fde6088920a0a71ab3cd9d5e6617c81a797c657f1fd9968", size = 607199 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/f4/124858007ddf3c61e9b144107304c9152fa80b5b6c168da07d86fe583cc1/huggingface_hub-1.1.5-py3-none-any.whl", hash = "sha256:e88ecc129011f37b868586bbcfae6c56868cae80cd56a79d61575426a3aa0d7d", size = 516000 }, +] + +[[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 = "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 = "jsonschema" +version = "4.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040 }, +] + +[[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 = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, +] + +[[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/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631 }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057 }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050 }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681 }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705 }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524 }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282 }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745 }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571 }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056 }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932 }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631 }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058 }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287 }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940 }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887 }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692 }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471 }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923 }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572 }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077 }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876 }, + { 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.22.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/a3/a2/c5ec0ab38b35ade2ae49a90fada718fbc76811dc5aa1760414c6aaa6b08a/mcp-1.22.0.tar.gz", hash = "sha256:769b9ac90ed42134375b19e777a2858ca300f95f2e800982b3e2be62dfc0ba01", size = 471788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/bb/711099f9c6bb52770f56e56401cdfb10da5b67029f701e0df29362df4c8e/mcp-1.22.0-py3-none-any.whl", hash = "sha256:bed758e24df1ed6846989c909ba4e3df339a27b4f30f1b8b627862a4bade4e98", size = 175489 }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.13'", + "python_full_version == '3.12.*'", + "python_full_version == '3.11.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641 }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324 }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872 }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148 }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282 }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903 }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672 }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896 }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608 }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555 }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873 }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838 }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559 }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702 }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086 }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985 }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976 }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274 }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922 }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667 }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251 }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652 }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172 }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990 }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902 }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430 }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551 }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275 }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637 }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090 }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710 }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292 }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897 }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391 }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275 }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855 }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359 }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374 }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587 }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940 }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341 }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507 }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706 }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507 }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049 }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603 }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696 }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350 }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190 }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749 }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432 }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388 }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651 }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503 }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612 }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042 }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502 }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962 }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054 }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613 }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147 }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806 }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760 }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459 }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689 }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053 }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635 }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768 }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263 }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213 }, +] + +[[package]] +name = "orjson" +version = "3.11.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/fe/ed708782d6709cc60eb4c2d8a361a440661f74134675c72990f2c48c785f/orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d", size = 5945188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/30/5aed63d5af1c8b02fbd2a8d83e2a6c8455e30504c50dbf08c8b51403d873/orjson-3.11.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1", size = 243870 }, + { url = "https://files.pythonhosted.org/packages/44/1f/da46563c08bef33c41fd63c660abcd2184b4d2b950c8686317d03b9f5f0c/orjson-3.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44", size = 130622 }, + { url = "https://files.pythonhosted.org/packages/02/bd/b551a05d0090eab0bf8008a13a14edc0f3c3e0236aa6f5b697760dd2817b/orjson-3.11.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c", size = 129344 }, + { url = "https://files.pythonhosted.org/packages/87/6c/9ddd5e609f443b2548c5e7df3c44d0e86df2c68587a0e20c50018cdec535/orjson-3.11.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23", size = 136633 }, + { url = "https://files.pythonhosted.org/packages/95/f2/9f04f2874c625a9fb60f6918c33542320661255323c272e66f7dcce14df2/orjson-3.11.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea", size = 137695 }, + { url = "https://files.pythonhosted.org/packages/d2/c2/c7302afcbdfe8a891baae0e2cee091583a30e6fa613e8bdf33b0e9c8a8c7/orjson-3.11.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba", size = 136879 }, + { url = "https://files.pythonhosted.org/packages/c6/3a/b31c8f0182a3e27f48e703f46e61bb769666cd0dac4700a73912d07a1417/orjson-3.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff", size = 136374 }, + { url = "https://files.pythonhosted.org/packages/29/d0/fd9ab96841b090d281c46df566b7f97bc6c8cd9aff3f3ebe99755895c406/orjson-3.11.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac", size = 140519 }, + { url = "https://files.pythonhosted.org/packages/d6/ce/36eb0f15978bb88e33a3480e1a3fb891caa0f189ba61ce7713e0ccdadabf/orjson-3.11.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79", size = 406522 }, + { url = "https://files.pythonhosted.org/packages/85/11/e8af3161a288f5c6a00c188fc729c7ba193b0cbc07309a1a29c004347c30/orjson-3.11.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827", size = 149790 }, + { url = "https://files.pythonhosted.org/packages/ea/96/209d52db0cf1e10ed48d8c194841e383e23c2ced5a2ee766649fe0e32d02/orjson-3.11.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b", size = 140040 }, + { url = "https://files.pythonhosted.org/packages/ef/0e/526db1395ccb74c3d59ac1660b9a325017096dc5643086b38f27662b4add/orjson-3.11.4-cp310-cp310-win32.whl", hash = "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3", size = 135955 }, + { url = "https://files.pythonhosted.org/packages/e6/69/18a778c9de3702b19880e73c9866b91cc85f904b885d816ba1ab318b223c/orjson-3.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc", size = 131577 }, + { url = "https://files.pythonhosted.org/packages/63/1d/1ea6005fffb56715fd48f632611e163d1604e8316a5bad2288bee9a1c9eb/orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39", size = 243498 }, + { url = "https://files.pythonhosted.org/packages/37/d7/ffed10c7da677f2a9da307d491b9eb1d0125b0307019c4ad3d665fd31f4f/orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d", size = 128961 }, + { url = "https://files.pythonhosted.org/packages/a2/96/3e4d10a18866d1368f73c8c44b7fe37cc8a15c32f2a7620be3877d4c55a3/orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175", size = 130321 }, + { url = "https://files.pythonhosted.org/packages/eb/1f/465f66e93f434f968dd74d5b623eb62c657bdba2332f5a8be9f118bb74c7/orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040", size = 129207 }, + { url = "https://files.pythonhosted.org/packages/28/43/d1e94837543321c119dff277ae8e348562fe8c0fafbb648ef7cb0c67e521/orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63", size = 136323 }, + { url = "https://files.pythonhosted.org/packages/bf/04/93303776c8890e422a5847dd012b4853cdd88206b8bbd3edc292c90102d1/orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9", size = 137440 }, + { url = "https://files.pythonhosted.org/packages/1e/ef/75519d039e5ae6b0f34d0336854d55544ba903e21bf56c83adc51cd8bf82/orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a", size = 136680 }, + { url = "https://files.pythonhosted.org/packages/b5/18/bf8581eaae0b941b44efe14fee7b7862c3382fbc9a0842132cfc7cf5ecf4/orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be", size = 136160 }, + { url = "https://files.pythonhosted.org/packages/c4/35/a6d582766d351f87fc0a22ad740a641b0a8e6fc47515e8614d2e4790ae10/orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7", size = 140318 }, + { url = "https://files.pythonhosted.org/packages/76/b3/5a4801803ab2e2e2d703bce1a56540d9f99a9143fbec7bf63d225044fef8/orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549", size = 406330 }, + { url = "https://files.pythonhosted.org/packages/80/55/a8f682f64833e3a649f620eafefee175cbfeb9854fc5b710b90c3bca45df/orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905", size = 149580 }, + { url = "https://files.pythonhosted.org/packages/ad/e4/c132fa0c67afbb3eb88274fa98df9ac1f631a675e7877037c611805a4413/orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907", size = 139846 }, + { url = "https://files.pythonhosted.org/packages/54/06/dc3491489efd651fef99c5908e13951abd1aead1257c67f16135f95ce209/orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c", size = 135781 }, + { url = "https://files.pythonhosted.org/packages/79/b7/5e5e8d77bd4ea02a6ac54c42c818afb01dd31961be8a574eb79f1d2cfb1e/orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a", size = 131391 }, + { url = "https://files.pythonhosted.org/packages/0f/dc/9484127cc1aa213be398ed735f5f270eedcb0c0977303a6f6ddc46b60204/orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045", size = 126252 }, + { url = "https://files.pythonhosted.org/packages/63/51/6b556192a04595b93e277a9ff71cd0cc06c21a7df98bcce5963fa0f5e36f/orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50", size = 243571 }, + { url = "https://files.pythonhosted.org/packages/1c/2c/2602392ddf2601d538ff11848b98621cd465d1a1ceb9db9e8043181f2f7b/orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853", size = 128891 }, + { url = "https://files.pythonhosted.org/packages/4e/47/bf85dcf95f7a3a12bf223394a4f849430acd82633848d52def09fa3f46ad/orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938", size = 130137 }, + { url = "https://files.pythonhosted.org/packages/b4/4d/a0cb31007f3ab6f1fd2a1b17057c7c349bc2baf8921a85c0180cc7be8011/orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415", size = 129152 }, + { url = "https://files.pythonhosted.org/packages/f7/ef/2811def7ce3d8576b19e3929fff8f8f0d44bc5eb2e0fdecb2e6e6cc6c720/orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44", size = 136834 }, + { url = "https://files.pythonhosted.org/packages/00/d4/9aee9e54f1809cec8ed5abd9bc31e8a9631d19460e3b8470145d25140106/orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2", size = 137519 }, + { url = "https://files.pythonhosted.org/packages/db/ea/67bfdb5465d5679e8ae8d68c11753aaf4f47e3e7264bad66dc2f2249e643/orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708", size = 136749 }, + { url = "https://files.pythonhosted.org/packages/01/7e/62517dddcfce6d53a39543cd74d0dccfcbdf53967017c58af68822100272/orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210", size = 136325 }, + { url = "https://files.pythonhosted.org/packages/18/ae/40516739f99ab4c7ec3aaa5cc242d341fcb03a45d89edeeaabc5f69cb2cf/orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241", size = 140204 }, + { url = "https://files.pythonhosted.org/packages/82/18/ff5734365623a8916e3a4037fcef1cd1782bfc14cf0992afe7940c5320bf/orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b", size = 406242 }, + { url = "https://files.pythonhosted.org/packages/e1/43/96436041f0a0c8c8deca6a05ebeaf529bf1de04839f93ac5e7c479807aec/orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c", size = 150013 }, + { url = "https://files.pythonhosted.org/packages/1b/48/78302d98423ed8780479a1e682b9aecb869e8404545d999d34fa486e573e/orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9", size = 139951 }, + { url = "https://files.pythonhosted.org/packages/4a/7b/ad613fdcdaa812f075ec0875143c3d37f8654457d2af17703905425981bf/orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa", size = 136049 }, + { url = "https://files.pythonhosted.org/packages/b9/3c/9cf47c3ff5f39b8350fb21ba65d789b6a1129d4cbb3033ba36c8a9023520/orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140", size = 131461 }, + { url = "https://files.pythonhosted.org/packages/c6/3b/e2425f61e5825dc5b08c2a5a2b3af387eaaca22a12b9c8c01504f8614c36/orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e", size = 126167 }, + { url = "https://files.pythonhosted.org/packages/23/15/c52aa7112006b0f3d6180386c3a46ae057f932ab3425bc6f6ac50431cca1/orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534", size = 243525 }, + { url = "https://files.pythonhosted.org/packages/ec/38/05340734c33b933fd114f161f25a04e651b0c7c33ab95e9416ade5cb44b8/orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff", size = 128871 }, + { url = "https://files.pythonhosted.org/packages/55/b9/ae8d34899ff0c012039b5a7cb96a389b2476e917733294e498586b45472d/orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad", size = 130055 }, + { url = "https://files.pythonhosted.org/packages/33/aa/6346dd5073730451bee3681d901e3c337e7ec17342fb79659ec9794fc023/orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5", size = 129061 }, + { url = "https://files.pythonhosted.org/packages/39/e4/8eea51598f66a6c853c380979912d17ec510e8e66b280d968602e680b942/orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a", size = 136541 }, + { url = "https://files.pythonhosted.org/packages/9a/47/cb8c654fa9adcc60e99580e17c32b9e633290e6239a99efa6b885aba9dbc/orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436", size = 137535 }, + { url = "https://files.pythonhosted.org/packages/43/92/04b8cc5c2b729f3437ee013ce14a60ab3d3001465d95c184758f19362f23/orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9", size = 136703 }, + { url = "https://files.pythonhosted.org/packages/aa/fd/d0733fcb9086b8be4ebcfcda2d0312865d17d0d9884378b7cffb29d0763f/orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73", size = 136293 }, + { url = "https://files.pythonhosted.org/packages/c2/d7/3c5514e806837c210492d72ae30ccf050ce3f940f45bf085bab272699ef4/orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0", size = 140131 }, + { url = "https://files.pythonhosted.org/packages/9c/dd/ba9d32a53207babf65bd510ac4d0faaa818bd0df9a9c6f472fe7c254f2e3/orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196", size = 406164 }, + { url = "https://files.pythonhosted.org/packages/8e/f9/f68ad68f4af7c7bde57cd514eaa2c785e500477a8bc8f834838eb696a685/orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a", size = 149859 }, + { url = "https://files.pythonhosted.org/packages/b6/d2/7f847761d0c26818395b3d6b21fb6bc2305d94612a35b0a30eae65a22728/orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6", size = 139926 }, + { url = "https://files.pythonhosted.org/packages/9f/37/acd14b12dc62db9a0e1d12386271b8661faae270b22492580d5258808975/orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839", size = 136007 }, + { url = "https://files.pythonhosted.org/packages/c0/a9/967be009ddf0a1fffd7a67de9c36656b28c763659ef91352acc02cbe364c/orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a", size = 131314 }, + { url = "https://files.pythonhosted.org/packages/cb/db/399abd6950fbd94ce125cb8cd1a968def95174792e127b0642781e040ed4/orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de", size = 126152 }, + { url = "https://files.pythonhosted.org/packages/25/e3/54ff63c093cc1697e758e4fceb53164dd2661a7d1bcd522260ba09f54533/orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803", size = 243501 }, + { url = "https://files.pythonhosted.org/packages/ac/7d/e2d1076ed2e8e0ae9badca65bf7ef22710f93887b29eaa37f09850604e09/orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54", size = 128862 }, + { url = "https://files.pythonhosted.org/packages/9f/37/ca2eb40b90621faddfa9517dfe96e25f5ae4d8057a7c0cdd613c17e07b2c/orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e", size = 130047 }, + { url = "https://files.pythonhosted.org/packages/c7/62/1021ed35a1f2bad9040f05fa4cc4f9893410df0ba3eaa323ccf899b1c90a/orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316", size = 129073 }, + { url = "https://files.pythonhosted.org/packages/e8/3f/f84d966ec2a6fd5f73b1a707e7cd876813422ae4bf9f0145c55c9c6a0f57/orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1", size = 136597 }, + { url = "https://files.pythonhosted.org/packages/32/78/4fa0aeca65ee82bbabb49e055bd03fa4edea33f7c080c5c7b9601661ef72/orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc", size = 137515 }, + { url = "https://files.pythonhosted.org/packages/c1/9d/0c102e26e7fde40c4c98470796d050a2ec1953897e2c8ab0cb95b0759fa2/orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f", size = 136703 }, + { url = "https://files.pythonhosted.org/packages/df/ac/2de7188705b4cdfaf0b6c97d2f7849c17d2003232f6e70df98602173f788/orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf", size = 136311 }, + { url = "https://files.pythonhosted.org/packages/e0/52/847fcd1a98407154e944feeb12e3b4d487a0e264c40191fb44d1269cbaa1/orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606", size = 140127 }, + { url = "https://files.pythonhosted.org/packages/c1/ae/21d208f58bdb847dd4d0d9407e2929862561841baa22bdab7aea10ca088e/orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780", size = 406201 }, + { url = "https://files.pythonhosted.org/packages/8d/55/0789d6de386c8366059db098a628e2ad8798069e94409b0d8935934cbcb9/orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23", size = 149872 }, + { url = "https://files.pythonhosted.org/packages/cc/1d/7ff81ea23310e086c17b41d78a72270d9de04481e6113dbe2ac19118f7fb/orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155", size = 139931 }, + { url = "https://files.pythonhosted.org/packages/77/92/25b886252c50ed64be68c937b562b2f2333b45afe72d53d719e46a565a50/orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394", size = 136065 }, + { url = "https://files.pythonhosted.org/packages/63/b8/718eecf0bb7e9d64e4956afaafd23db9f04c776d445f59fe94f54bdae8f0/orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1", size = 131310 }, + { url = "https://files.pythonhosted.org/packages/1a/bf/def5e25d4d8bfce296a9a7c8248109bf58622c21618b590678f945a2c59c/orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d", size = 126151 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { 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/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763 }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217 }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791 }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373 }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444 }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459 }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086 }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790 }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831 }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267 }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281 }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453 }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361 }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702 }, + { 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 = "pillow" +version = "11.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554 }, + { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548 }, + { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742 }, + { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087 }, + { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350 }, + { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840 }, + { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005 }, + { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372 }, + { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090 }, + { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988 }, + { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899 }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531 }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560 }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978 }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168 }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053 }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273 }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043 }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516 }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768 }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055 }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079 }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800 }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296 }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726 }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652 }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787 }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236 }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950 }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358 }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079 }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324 }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067 }, + { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328 }, + { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652 }, + { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443 }, + { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474 }, + { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038 }, + { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407 }, + { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094 }, + { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503 }, + { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574 }, + { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060 }, + { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407 }, + { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841 }, + { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450 }, + { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055 }, + { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110 }, + { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547 }, + { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554 }, + { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132 }, + { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001 }, + { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814 }, + { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124 }, + { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186 }, + { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546 }, + { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102 }, + { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803 }, + { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520 }, + { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116 }, + { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597 }, + { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246 }, + { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336 }, + { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699 }, + { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789 }, + { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386 }, + { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911 }, + { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383 }, + { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385 }, + { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129 }, + { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580 }, + { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860 }, + { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694 }, + { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888 }, + { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330 }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089 }, + { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206 }, + { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370 }, + { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500 }, + { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835 }, + { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556 }, + { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625 }, + { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207 }, + { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939 }, + { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166 }, + { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482 }, + { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596 }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566 }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618 }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248 }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963 }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170 }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505 }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598 }, +] + +[[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.3" +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/f3/1e/4f0a3233767010308f2fd6bd0814597e3f63f1dc98304a9112b8759df4ff/pydantic-2.12.3.tar.gz", hash = "sha256:1da1c82b0fc140bb0103bc1441ffe062154c8d38491189751ee00fd8ca65ce74", size = 819383 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/6b/83661fa77dcefa195ad5f8cd9af3d1a7450fd57cc883ad04d65446ac2029/pydantic-2.12.3-py3-none-any.whl", hash = "sha256:6986454a854bc3bc6e5443e1369e06a3a456af9d339eda45510f517d9ea5c6bf", size = 462431 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/3d/9b8ca77b0f76fcdbf8bc6b72474e264283f461284ca84ac3fde570c6c49a/pydantic_core-2.41.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2442d9a4d38f3411f22eb9dd0912b7cbf4b7d5b6c92c4173b75d3e1ccd84e36e", size = 2111197 }, + { url = "https://files.pythonhosted.org/packages/59/92/b7b0fe6ed4781642232755cb7e56a86e2041e1292f16d9ae410a0ccee5ac/pydantic_core-2.41.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30a9876226dda131a741afeab2702e2d127209bde3c65a2b8133f428bc5d006b", size = 1917909 }, + { url = "https://files.pythonhosted.org/packages/52/8c/3eb872009274ffa4fb6a9585114e161aa1a0915af2896e2d441642929fe4/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d55bbac04711e2980645af68b97d445cdbcce70e5216de444a6c4b6943ebcccd", size = 1969905 }, + { url = "https://files.pythonhosted.org/packages/f4/21/35adf4a753bcfaea22d925214a0c5b880792e3244731b3f3e6fec0d124f7/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1d778fb7849a42d0ee5927ab0f7453bf9f85eef8887a546ec87db5ddb178945", size = 2051938 }, + { url = "https://files.pythonhosted.org/packages/7d/d0/cdf7d126825e36d6e3f1eccf257da8954452934ede275a8f390eac775e89/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b65077a4693a98b90ec5ad8f203ad65802a1b9b6d4a7e48066925a7e1606706", size = 2250710 }, + { url = "https://files.pythonhosted.org/packages/2e/1c/af1e6fd5ea596327308f9c8d1654e1285cc3d8de0d584a3c9d7705bf8a7c/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62637c769dee16eddb7686bf421be48dfc2fae93832c25e25bc7242e698361ba", size = 2367445 }, + { url = "https://files.pythonhosted.org/packages/d3/81/8cece29a6ef1b3a92f956ea6da6250d5b2d2e7e4d513dd3b4f0c7a83dfea/pydantic_core-2.41.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dfe3aa529c8f501babf6e502936b9e8d4698502b2cfab41e17a028d91b1ac7b", size = 2072875 }, + { url = "https://files.pythonhosted.org/packages/e3/37/a6a579f5fc2cd4d5521284a0ab6a426cc6463a7b3897aeb95b12f1ba607b/pydantic_core-2.41.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca2322da745bf2eeb581fc9ea3bbb31147702163ccbcbf12a3bb630e4bf05e1d", size = 2191329 }, + { url = "https://files.pythonhosted.org/packages/ae/03/505020dc5c54ec75ecba9f41119fd1e48f9e41e4629942494c4a8734ded1/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e8cd3577c796be7231dcf80badcf2e0835a46665eaafd8ace124d886bab4d700", size = 2151658 }, + { url = "https://files.pythonhosted.org/packages/cb/5d/2c0d09fb53aa03bbd2a214d89ebfa6304be7df9ed86ee3dc7770257f41ee/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:1cae8851e174c83633f0833e90636832857297900133705ee158cf79d40f03e6", size = 2316777 }, + { url = "https://files.pythonhosted.org/packages/ea/4b/c2c9c8f5e1f9c864b57d08539d9d3db160e00491c9f5ee90e1bfd905e644/pydantic_core-2.41.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a26d950449aae348afe1ac8be5525a00ae4235309b729ad4d3399623125b43c9", size = 2320705 }, + { url = "https://files.pythonhosted.org/packages/28/c3/a74c1c37f49c0a02c89c7340fafc0ba816b29bd495d1a31ce1bdeacc6085/pydantic_core-2.41.4-cp310-cp310-win32.whl", hash = "sha256:0cf2a1f599efe57fa0051312774280ee0f650e11152325e41dfd3018ef2c1b57", size = 1975464 }, + { url = "https://files.pythonhosted.org/packages/d6/23/5dd5c1324ba80303368f7569e2e2e1a721c7d9eb16acb7eb7b7f85cb1be2/pydantic_core-2.41.4-cp310-cp310-win_amd64.whl", hash = "sha256:a8c2e340d7e454dc3340d3d2e8f23558ebe78c98aa8f68851b04dcb7bc37abdc", size = 2024497 }, + { url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062 }, + { url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301 }, + { url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728 }, + { url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238 }, + { url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424 }, + { url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047 }, + { url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163 }, + { url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585 }, + { url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109 }, + { url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078 }, + { url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737 }, + { url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160 }, + { url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883 }, + { url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026 }, + { url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043 }, + { url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699 }, + { url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121 }, + { url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590 }, + { url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869 }, + { url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169 }, + { url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165 }, + { url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067 }, + { url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997 }, + { url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187 }, + { url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204 }, + { url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536 }, + { url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132 }, + { url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483 }, + { url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688 }, + { url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807 }, + { url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669 }, + { url = "https://files.pythonhosted.org/packages/60/a4/24271cc71a17f64589be49ab8bd0751f6a0a03046c690df60989f2f95c2c/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de7c42f897e689ee6f9e93c4bec72b99ae3b32a2ade1c7e4798e690ff5246e02", size = 2051629 }, + { url = "https://files.pythonhosted.org/packages/68/de/45af3ca2f175d91b96bfb62e1f2d2f1f9f3b14a734afe0bfeff079f78181/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:664b3199193262277b8b3cd1e754fb07f2c6023289c815a1e1e8fb415cb247b1", size = 2224049 }, + { url = "https://files.pythonhosted.org/packages/af/8f/ae4e1ff84672bf869d0a77af24fd78387850e9497753c432875066b5d622/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d95b253b88f7d308b1c0b417c4624f44553ba4762816f94e6986819b9c273fb2", size = 2342409 }, + { url = "https://files.pythonhosted.org/packages/18/62/273dd70b0026a085c7b74b000394e1ef95719ea579c76ea2f0cc8893736d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1351f5bbdbbabc689727cb91649a00cb9ee7203e0a6e54e9f5ba9e22e384b84", size = 2069635 }, + { url = "https://files.pythonhosted.org/packages/30/03/cf485fff699b4cdaea469bc481719d3e49f023241b4abb656f8d422189fc/pydantic_core-2.41.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1affa4798520b148d7182da0615d648e752de4ab1a9566b7471bc803d88a062d", size = 2194284 }, + { url = "https://files.pythonhosted.org/packages/f9/7e/c8e713db32405dfd97211f2fc0a15d6bf8adb7640f3d18544c1f39526619/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7b74e18052fea4aa8dea2fb7dbc23d15439695da6cbe6cfc1b694af1115df09d", size = 2137566 }, + { url = "https://files.pythonhosted.org/packages/04/f7/db71fd4cdccc8b75990f79ccafbbd66757e19f6d5ee724a6252414483fb4/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:285b643d75c0e30abda9dc1077395624f314a37e3c09ca402d4015ef5979f1a2", size = 2316809 }, + { url = "https://files.pythonhosted.org/packages/76/63/a54973ddb945f1bca56742b48b144d85c9fc22f819ddeb9f861c249d5464/pydantic_core-2.41.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f52679ff4218d713b3b33f88c89ccbf3a5c2c12ba665fb80ccc4192b4608dbab", size = 2311119 }, + { url = "https://files.pythonhosted.org/packages/f8/03/5d12891e93c19218af74843a27e32b94922195ded2386f7b55382f904d2f/pydantic_core-2.41.4-cp313-cp313-win32.whl", hash = "sha256:ecde6dedd6fff127c273c76821bb754d793be1024bc33314a120f83a3c69460c", size = 1981398 }, + { url = "https://files.pythonhosted.org/packages/be/d8/fd0de71f39db91135b7a26996160de71c073d8635edfce8b3c3681be0d6d/pydantic_core-2.41.4-cp313-cp313-win_amd64.whl", hash = "sha256:d081a1f3800f05409ed868ebb2d74ac39dd0c1ff6c035b5162356d76030736d4", size = 2030735 }, + { url = "https://files.pythonhosted.org/packages/72/86/c99921c1cf6650023c08bfab6fe2d7057a5142628ef7ccfa9921f2dda1d5/pydantic_core-2.41.4-cp313-cp313-win_arm64.whl", hash = "sha256:f8e49c9c364a7edcbe2a310f12733aad95b022495ef2a8d653f645e5d20c1564", size = 1973209 }, + { url = "https://files.pythonhosted.org/packages/36/0d/b5706cacb70a8414396efdda3d72ae0542e050b591119e458e2490baf035/pydantic_core-2.41.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ed97fd56a561f5eb5706cebe94f1ad7c13b84d98312a05546f2ad036bafe87f4", size = 1877324 }, + { url = "https://files.pythonhosted.org/packages/de/2d/cba1fa02cfdea72dfb3a9babb067c83b9dff0bbcb198368e000a6b756ea7/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a870c307bf1ee91fc58a9a61338ff780d01bfae45922624816878dce784095d2", size = 1884515 }, + { url = "https://files.pythonhosted.org/packages/07/ea/3df927c4384ed9b503c9cc2d076cf983b4f2adb0c754578dfb1245c51e46/pydantic_core-2.41.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25e97bc1f5f8f7985bdc2335ef9e73843bb561eb1fa6831fdfc295c1c2061cf", size = 2042819 }, + { url = "https://files.pythonhosted.org/packages/6a/ee/df8e871f07074250270a3b1b82aad4cd0026b588acd5d7d3eb2fcb1471a3/pydantic_core-2.41.4-cp313-cp313t-win_amd64.whl", hash = "sha256:d405d14bea042f166512add3091c1af40437c2e7f86988f3915fabd27b1e9cd2", size = 1995866 }, + { url = "https://files.pythonhosted.org/packages/fc/de/b20f4ab954d6d399499c33ec4fafc46d9551e11dc1858fb7f5dca0748ceb/pydantic_core-2.41.4-cp313-cp313t-win_arm64.whl", hash = "sha256:19f3684868309db5263a11bace3c45d93f6f24afa2ffe75a647583df22a2ff89", size = 1970034 }, + { url = "https://files.pythonhosted.org/packages/54/28/d3325da57d413b9819365546eb9a6e8b7cbd9373d9380efd5f74326143e6/pydantic_core-2.41.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:e9205d97ed08a82ebb9a307e92914bb30e18cdf6f6b12ca4bedadb1588a0bfe1", size = 2102022 }, + { url = "https://files.pythonhosted.org/packages/9e/24/b58a1bc0d834bf1acc4361e61233ee217169a42efbdc15a60296e13ce438/pydantic_core-2.41.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:82df1f432b37d832709fbcc0e24394bba04a01b6ecf1ee87578145c19cde12ac", size = 1905495 }, + { url = "https://files.pythonhosted.org/packages/fb/a4/71f759cc41b7043e8ecdaab81b985a9b6cad7cec077e0b92cff8b71ecf6b/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc3b4cc4539e055cfa39a3763c939f9d409eb40e85813257dcd761985a108554", size = 1956131 }, + { url = "https://files.pythonhosted.org/packages/b0/64/1e79ac7aa51f1eec7c4cda8cbe456d5d09f05fdd68b32776d72168d54275/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b1eb1754fce47c63d2ff57fdb88c351a6c0150995890088b33767a10218eaa4e", size = 2052236 }, + { url = "https://files.pythonhosted.org/packages/e9/e3/a3ffc363bd4287b80f1d43dc1c28ba64831f8dfc237d6fec8f2661138d48/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6ab5ab30ef325b443f379ddb575a34969c333004fca5a1daa0133a6ffaad616", size = 2223573 }, + { url = "https://files.pythonhosted.org/packages/28/27/78814089b4d2e684a9088ede3790763c64693c3d1408ddc0a248bc789126/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:31a41030b1d9ca497634092b46481b937ff9397a86f9f51bd41c4767b6fc04af", size = 2342467 }, + { url = "https://files.pythonhosted.org/packages/92/97/4de0e2a1159cb85ad737e03306717637842c88c7fd6d97973172fb183149/pydantic_core-2.41.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a44ac1738591472c3d020f61c6df1e4015180d6262ebd39bf2aeb52571b60f12", size = 2063754 }, + { url = "https://files.pythonhosted.org/packages/0f/50/8cb90ce4b9efcf7ae78130afeb99fd1c86125ccdf9906ef64b9d42f37c25/pydantic_core-2.41.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d72f2b5e6e82ab8f94ea7d0d42f83c487dc159c5240d8f83beae684472864e2d", size = 2196754 }, + { url = "https://files.pythonhosted.org/packages/34/3b/ccdc77af9cd5082723574a1cc1bcae7a6acacc829d7c0a06201f7886a109/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c4d1e854aaf044487d31143f541f7aafe7b482ae72a022c664b2de2e466ed0ad", size = 2137115 }, + { url = "https://files.pythonhosted.org/packages/ca/ba/e7c7a02651a8f7c52dc2cff2b64a30c313e3b57c7d93703cecea76c09b71/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b568af94267729d76e6ee5ececda4e283d07bbb28e8148bb17adad93d025d25a", size = 2317400 }, + { url = "https://files.pythonhosted.org/packages/2c/ba/6c533a4ee8aec6b812c643c49bb3bd88d3f01e3cebe451bb85512d37f00f/pydantic_core-2.41.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:6d55fb8b1e8929b341cc313a81a26e0d48aa3b519c1dbaadec3a6a2b4fcad025", size = 2312070 }, + { url = "https://files.pythonhosted.org/packages/22/ae/f10524fcc0ab8d7f96cf9a74c880243576fd3e72bd8ce4f81e43d22bcab7/pydantic_core-2.41.4-cp314-cp314-win32.whl", hash = "sha256:5b66584e549e2e32a1398df11da2e0a7eff45d5c2d9db9d5667c5e6ac764d77e", size = 1982277 }, + { url = "https://files.pythonhosted.org/packages/b4/dc/e5aa27aea1ad4638f0c3fb41132f7eb583bd7420ee63204e2d4333a3bbf9/pydantic_core-2.41.4-cp314-cp314-win_amd64.whl", hash = "sha256:557a0aab88664cc552285316809cab897716a372afaf8efdbef756f8b890e894", size = 2024608 }, + { url = "https://files.pythonhosted.org/packages/3e/61/51d89cc2612bd147198e120a13f150afbf0bcb4615cddb049ab10b81b79e/pydantic_core-2.41.4-cp314-cp314-win_arm64.whl", hash = "sha256:3f1ea6f48a045745d0d9f325989d8abd3f1eaf47dd00485912d1a3a63c623a8d", size = 1967614 }, + { url = "https://files.pythonhosted.org/packages/0d/c2/472f2e31b95eff099961fa050c376ab7156a81da194f9edb9f710f68787b/pydantic_core-2.41.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6c1fe4c5404c448b13188dd8bd2ebc2bdd7e6727fa61ff481bcc2cca894018da", size = 1876904 }, + { url = "https://files.pythonhosted.org/packages/4a/07/ea8eeb91173807ecdae4f4a5f4b150a520085b35454350fc219ba79e66a3/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:523e7da4d43b113bf8e7b49fa4ec0c35bf4fe66b2230bfc5c13cc498f12c6c3e", size = 1882538 }, + { url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183 }, + { url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542 }, + { url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897 }, + { url = "https://files.pythonhosted.org/packages/5d/d4/912e976a2dd0b49f31c98a060ca90b353f3b73ee3ea2fd0030412f6ac5ec/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e5ab4fc177dd41536b3c32b2ea11380dd3d4619a385860621478ac2d25ceb00", size = 2106739 }, + { url = "https://files.pythonhosted.org/packages/71/f0/66ec5a626c81eba326072d6ee2b127f8c139543f1bf609b4842978d37833/pydantic_core-2.41.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:3d88d0054d3fa11ce936184896bed3c1c5441d6fa483b498fac6a5d0dd6f64a9", size = 1932549 }, + { url = "https://files.pythonhosted.org/packages/c4/af/625626278ca801ea0a658c2dcf290dc9f21bb383098e99e7c6a029fccfc0/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2a054a8725f05b4b6503357e0ac1c4e8234ad3b0c2ac130d6ffc66f0e170e2", size = 2135093 }, + { url = "https://files.pythonhosted.org/packages/20/f6/2fba049f54e0f4975fef66be654c597a1d005320fa141863699180c7697d/pydantic_core-2.41.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0d9db5a161c99375a0c68c058e227bee1d89303300802601d76a3d01f74e258", size = 2187971 }, + { url = "https://files.pythonhosted.org/packages/0e/80/65ab839a2dfcd3b949202f9d920c34f9de5a537c3646662bdf2f7d999680/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:6273ea2c8ffdac7b7fda2653c49682db815aebf4a89243a6feccf5e36c18c347", size = 2147939 }, + { url = "https://files.pythonhosted.org/packages/44/58/627565d3d182ce6dfda18b8e1c841eede3629d59c9d7cbc1e12a03aeb328/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:4c973add636efc61de22530b2ef83a65f39b6d6f656df97f678720e20de26caa", size = 2311400 }, + { url = "https://files.pythonhosted.org/packages/24/06/8a84711162ad5a5f19a88cead37cca81b4b1f294f46260ef7334ae4f24d3/pydantic_core-2.41.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b69d1973354758007f46cf2d44a4f3d0933f10b6dc9bf15cf1356e037f6f731a", size = 2316840 }, + { url = "https://files.pythonhosted.org/packages/aa/8b/b7bb512a4682a2f7fbfae152a755d37351743900226d29bd953aaf870eaa/pydantic_core-2.41.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3619320641fd212aaf5997b6ca505e97540b7e16418f4a241f44cdf108ffb50d", size = 2149135 }, + { url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721 }, + { url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608 }, + { url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986 }, + { url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516 }, + { url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146 }, + { url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296 }, + { url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386 }, + { url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775 }, +] + +[[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 = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327 }, +] + +[[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 = "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.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546 }, +] + +[[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/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432 }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103 }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557 }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031 }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308 }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930 }, + { 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/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227 }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019 }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646 }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793 }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293 }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872 }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828 }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415 }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561 }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826 }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577 }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556 }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114 }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638 }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463 }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986 }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543 }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763 }, + { 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 = "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 = "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 = "rich" +version = "14.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393 }, +] + +[[package]] +name = "rpds-py" +version = "0.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/33/23b3b3419b6a3e0f559c7c0d2ca8fc1b9448382b25245033788785921332/rpds_py-0.29.0.tar.gz", hash = "sha256:fe55fe686908f50154d1dc599232016e50c243b438c3b7432f24e2895b0e5359", size = 69359 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/7a/c5b2ff381b74bc742768e8d870f26babac4ef256ba160bdbf8d57af56461/rpds_py-0.29.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4ae4b88c6617e1b9e5038ab3fccd7bac0842fdda2b703117b2aa99bc85379113", size = 372385 }, + { url = "https://files.pythonhosted.org/packages/28/36/531f1eb4d5bed4a9c150f363a7ec4a98d2dc746151bba5473bc38ee85dec/rpds_py-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7d9128ec9d8cecda6f044001fde4fb71ea7c24325336612ef8179091eb9596b9", size = 362869 }, + { url = "https://files.pythonhosted.org/packages/54/df/7e9c0493a2015d9c82807a2d5f023ea9774e27a4c15b33ef1cdb7456138d/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37812c3da8e06f2bb35b3cf10e4a7b68e776a706c13058997238762b4e07f4f", size = 391582 }, + { url = "https://files.pythonhosted.org/packages/15/38/42a981c3592ef46fbd7e17adbf8730cc5ec87e6aa1770c658c44bbb52960/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66786c3fb1d8de416a7fa8e1cb1ec6ba0a745b2b0eee42f9b7daa26f1a495545", size = 405685 }, + { url = "https://files.pythonhosted.org/packages/12/45/628b8c15856c3849c3f52ec6dac93c046ed5faeed4a435af03b70525fd29/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b58f5c77f1af888b5fd1876c9a0d9858f6f88a39c9dd7c073a88e57e577da66d", size = 527067 }, + { url = "https://files.pythonhosted.org/packages/dc/ba/6b56d09badeabd95098016d72a437d4a0fd82d4672ce92a7607df5d70a42/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:799156ef1f3529ed82c36eb012b5d7a4cf4b6ef556dd7cc192148991d07206ae", size = 412532 }, + { url = "https://files.pythonhosted.org/packages/f1/39/2f1f3db92888314b50b8f9641f679188bd24b3665a8cb9923b7201ae8011/rpds_py-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453783477aa4f2d9104c4b59b08c871431647cb7af51b549bbf2d9eb9c827756", size = 392736 }, + { url = "https://files.pythonhosted.org/packages/60/43/3c3b1dcd827e50f2ae28786d846b8a351080d8a69a3b49bc10ae44cc39b1/rpds_py-0.29.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:24a7231493e3c4a4b30138b50cca089a598e52c34cf60b2f35cebf62f274fdea", size = 406300 }, + { url = "https://files.pythonhosted.org/packages/da/02/bc96021b67f8525e6bcdd68935c4543ada61e1f3dcb067ed037d68b8c6d2/rpds_py-0.29.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7033c1010b1f57bb44d8067e8c25aa6fa2e944dbf46ccc8c92b25043839c3fd2", size = 423641 }, + { url = "https://files.pythonhosted.org/packages/38/e9/c435ddb602ced19a80b8277a41371734f33ad3f91cc4ceb4d82596800a3c/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0248b19405422573621172ab8e3a1f29141362d13d9f72bafa2e28ea0cdca5a2", size = 574153 }, + { url = "https://files.pythonhosted.org/packages/84/82/dc3c32e1f89ecba8a59600d4cd65fe0ad81b6c636ccdbf6cd177fd6a7bac/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f9f436aee28d13b9ad2c764fc273e0457e37c2e61529a07b928346b219fcde3b", size = 600304 }, + { url = "https://files.pythonhosted.org/packages/35/98/785290e0b7142470735dc1b1f68fb33aae29e5296f062c88396eedf796c8/rpds_py-0.29.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24a16cb7163933906c62c272de20ea3c228e4542c8c45c1d7dc2b9913e17369a", size = 562211 }, + { url = "https://files.pythonhosted.org/packages/30/58/4eeddcb0737c6875f3e30c65dc9d7e7a10dfd5779646a990fa602c6d56c5/rpds_py-0.29.0-cp310-cp310-win32.whl", hash = "sha256:1a409b0310a566bfd1be82119891fefbdce615ccc8aa558aff7835c27988cbef", size = 221803 }, + { url = "https://files.pythonhosted.org/packages/54/77/b35a8dbdcbeb32505500547cdafaa9f8863e85f8faac50ef34464ec5a256/rpds_py-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:c5523b0009e7c3c1263471b69d8da1c7d41b3ecb4cb62ef72be206b92040a950", size = 235530 }, + { url = "https://files.pythonhosted.org/packages/36/ab/7fb95163a53ab122c74a7c42d2d2f012819af2cf3deb43fb0d5acf45cc1a/rpds_py-0.29.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9b9c764a11fd637e0322a488560533112837f5334ffeb48b1be20f6d98a7b437", size = 372344 }, + { url = "https://files.pythonhosted.org/packages/b3/45/f3c30084c03b0d0f918cb4c5ae2c20b0a148b51ba2b3f6456765b629bedd/rpds_py-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3fd2164d73812026ce970d44c3ebd51e019d2a26a4425a5dcbdfa93a34abc383", size = 363041 }, + { url = "https://files.pythonhosted.org/packages/e3/e9/4d044a1662608c47a87cbb37b999d4d5af54c6d6ebdda93a4d8bbf8b2a10/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a097b7f7f7274164566ae90a221fd725363c0e9d243e2e9ed43d195ccc5495c", size = 391775 }, + { url = "https://files.pythonhosted.org/packages/50/c9/7616d3ace4e6731aeb6e3cd85123e03aec58e439044e214b9c5c60fd8eb1/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cdc0490374e31cedefefaa1520d5fe38e82fde8748cbc926e7284574c714d6b", size = 405624 }, + { url = "https://files.pythonhosted.org/packages/c2/e2/6d7d6941ca0843609fd2d72c966a438d6f22617baf22d46c3d2156c31350/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89ca2e673ddd5bde9b386da9a0aac0cab0e76f40c8f0aaf0d6311b6bbf2aa311", size = 527894 }, + { url = "https://files.pythonhosted.org/packages/8d/f7/aee14dc2db61bb2ae1e3068f134ca9da5f28c586120889a70ff504bb026f/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5d9da3ff5af1ca1249b1adb8ef0573b94c76e6ae880ba1852f033bf429d4588", size = 412720 }, + { url = "https://files.pythonhosted.org/packages/2f/e2/2293f236e887c0360c2723d90c00d48dee296406994d6271faf1712e94ec/rpds_py-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8238d1d310283e87376c12f658b61e1ee23a14c0e54c7c0ce953efdbdc72deed", size = 392945 }, + { url = "https://files.pythonhosted.org/packages/14/cd/ceea6147acd3bd1fd028d1975228f08ff19d62098078d5ec3eed49703797/rpds_py-0.29.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:2d6fb2ad1c36f91c4646989811e84b1ea5e0c3cf9690b826b6e32b7965853a63", size = 406385 }, + { url = "https://files.pythonhosted.org/packages/52/36/fe4dead19e45eb77a0524acfdbf51e6cda597b26fc5b6dddbff55fbbb1a5/rpds_py-0.29.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:534dc9df211387547267ccdb42253aa30527482acb38dd9b21c5c115d66a96d2", size = 423943 }, + { url = "https://files.pythonhosted.org/packages/a1/7b/4551510803b582fa4abbc8645441a2d15aa0c962c3b21ebb380b7e74f6a1/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d456e64724a075441e4ed648d7f154dc62e9aabff29bcdf723d0c00e9e1d352f", size = 574204 }, + { url = "https://files.pythonhosted.org/packages/64/ba/071ccdd7b171e727a6ae079f02c26f75790b41555f12ca8f1151336d2124/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a738f2da2f565989401bd6fd0b15990a4d1523c6d7fe83f300b7e7d17212feca", size = 600587 }, + { url = "https://files.pythonhosted.org/packages/03/09/96983d48c8cf5a1e03c7d9cc1f4b48266adfb858ae48c7c2ce978dbba349/rpds_py-0.29.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a110e14508fd26fd2e472bb541f37c209409876ba601cf57e739e87d8a53cf95", size = 562287 }, + { url = "https://files.pythonhosted.org/packages/40/f0/8c01aaedc0fa92156f0391f39ea93b5952bc0ec56b897763858f95da8168/rpds_py-0.29.0-cp311-cp311-win32.whl", hash = "sha256:923248a56dd8d158389a28934f6f69ebf89f218ef96a6b216a9be6861804d3f4", size = 221394 }, + { url = "https://files.pythonhosted.org/packages/7e/a5/a8b21c54c7d234efdc83dc034a4d7cd9668e3613b6316876a29b49dece71/rpds_py-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:539eb77eb043afcc45314d1be09ea6d6cafb3addc73e0547c171c6d636957f60", size = 235713 }, + { url = "https://files.pythonhosted.org/packages/a7/1f/df3c56219523947b1be402fa12e6323fe6d61d883cf35d6cb5d5bb6db9d9/rpds_py-0.29.0-cp311-cp311-win_arm64.whl", hash = "sha256:bdb67151ea81fcf02d8f494703fb728d4d34d24556cbff5f417d74f6f5792e7c", size = 229157 }, + { url = "https://files.pythonhosted.org/packages/3c/50/bc0e6e736d94e420df79be4deb5c9476b63165c87bb8f19ef75d100d21b3/rpds_py-0.29.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0891cfd8db43e085c0ab93ab7e9b0c8fee84780d436d3b266b113e51e79f954", size = 376000 }, + { url = "https://files.pythonhosted.org/packages/3e/3a/46676277160f014ae95f24de53bed0e3b7ea66c235e7de0b9df7bd5d68ba/rpds_py-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3897924d3f9a0361472d884051f9a2460358f9a45b1d85a39a158d2f8f1ad71c", size = 360575 }, + { url = "https://files.pythonhosted.org/packages/75/ba/411d414ed99ea1afdd185bbabeeaac00624bd1e4b22840b5e9967ade6337/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a21deb8e0d1571508c6491ce5ea5e25669b1dd4adf1c9d64b6314842f708b5d", size = 392159 }, + { url = "https://files.pythonhosted.org/packages/8f/b1/e18aa3a331f705467a48d0296778dc1fea9d7f6cf675bd261f9a846c7e90/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9efe71687d6427737a0a2de9ca1c0a216510e6cd08925c44162be23ed7bed2d5", size = 410602 }, + { url = "https://files.pythonhosted.org/packages/2f/6c/04f27f0c9f2299274c76612ac9d2c36c5048bb2c6c2e52c38c60bf3868d9/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40f65470919dc189c833e86b2c4bd21bd355f98436a2cef9e0a9a92aebc8e57e", size = 515808 }, + { url = "https://files.pythonhosted.org/packages/83/56/a8412aa464fb151f8bc0d91fb0bb888adc9039bd41c1c6ba8d94990d8cf8/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:def48ff59f181130f1a2cb7c517d16328efac3ec03951cca40c1dc2049747e83", size = 416015 }, + { url = "https://files.pythonhosted.org/packages/04/4c/f9b8a05faca3d9e0a6397c90d13acb9307c9792b2bff621430c58b1d6e76/rpds_py-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad7bd570be92695d89285a4b373006930715b78d96449f686af422debb4d3949", size = 395325 }, + { url = "https://files.pythonhosted.org/packages/34/60/869f3bfbf8ed7b54f1ad9a5543e0fdffdd40b5a8f587fe300ee7b4f19340/rpds_py-0.29.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:5a572911cd053137bbff8e3a52d31c5d2dba51d3a67ad902629c70185f3f2181", size = 410160 }, + { url = "https://files.pythonhosted.org/packages/91/aa/e5b496334e3aba4fe4c8a80187b89f3c1294c5c36f2a926da74338fa5a73/rpds_py-0.29.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d583d4403bcbf10cffc3ab5cee23d7643fcc960dff85973fd3c2d6c86e8dbb0c", size = 425309 }, + { url = "https://files.pythonhosted.org/packages/85/68/4e24a34189751ceb6d66b28f18159922828dd84155876551f7ca5b25f14f/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:070befbb868f257d24c3bb350dbd6e2f645e83731f31264b19d7231dd5c396c7", size = 574644 }, + { url = "https://files.pythonhosted.org/packages/8c/cf/474a005ea4ea9c3b4f17b6108b6b13cebfc98ebaff11d6e1b193204b3a93/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fc935f6b20b0c9f919a8ff024739174522abd331978f750a74bb68abd117bd19", size = 601605 }, + { url = "https://files.pythonhosted.org/packages/f4/b1/c56f6a9ab8c5f6bb5c65c4b5f8229167a3a525245b0773f2c0896686b64e/rpds_py-0.29.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8c5a8ecaa44ce2d8d9d20a68a2483a74c07f05d72e94a4dff88906c8807e77b0", size = 564593 }, + { url = "https://files.pythonhosted.org/packages/b3/13/0494cecce4848f68501e0a229432620b4b57022388b071eeff95f3e1e75b/rpds_py-0.29.0-cp312-cp312-win32.whl", hash = "sha256:ba5e1aeaf8dd6d8f6caba1f5539cddda87d511331714b7b5fc908b6cfc3636b7", size = 223853 }, + { url = "https://files.pythonhosted.org/packages/1f/6a/51e9aeb444a00cdc520b032a28b07e5f8dc7bc328b57760c53e7f96997b4/rpds_py-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:b5f6134faf54b3cb83375db0f113506f8b7770785be1f95a631e7e2892101977", size = 239895 }, + { url = "https://files.pythonhosted.org/packages/d1/d4/8bce56cdad1ab873e3f27cb31c6a51d8f384d66b022b820525b879f8bed1/rpds_py-0.29.0-cp312-cp312-win_arm64.whl", hash = "sha256:b016eddf00dca7944721bf0cd85b6af7f6c4efaf83ee0b37c4133bd39757a8c7", size = 230321 }, + { url = "https://files.pythonhosted.org/packages/fd/d9/c5de60d9d371bbb186c3e9bf75f4fc5665e11117a25a06a6b2e0afb7380e/rpds_py-0.29.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1585648d0760b88292eecab5181f5651111a69d90eff35d6b78aa32998886a61", size = 375710 }, + { url = "https://files.pythonhosted.org/packages/b3/b3/0860cdd012291dc21272895ce107f1e98e335509ba986dd83d72658b82b9/rpds_py-0.29.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:521807963971a23996ddaf764c682b3e46459b3c58ccd79fefbe16718db43154", size = 360582 }, + { url = "https://files.pythonhosted.org/packages/92/8a/a18c2f4a61b3407e56175f6aab6deacdf9d360191a3d6f38566e1eaf7266/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a8896986efaa243ab713c69e6491a4138410f0fe36f2f4c71e18bd5501e8014", size = 391172 }, + { url = "https://files.pythonhosted.org/packages/fd/49/e93354258508c50abc15cdcd5fcf7ac4117f67bb6233ad7859f75e7372a0/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1d24564a700ef41480a984c5ebed62b74e6ce5860429b98b1fede76049e953e6", size = 409586 }, + { url = "https://files.pythonhosted.org/packages/5a/8d/a27860dae1c19a6bdc901f90c81f0d581df1943355802961a57cdb5b6cd1/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6596b93c010d386ae46c9fba9bfc9fc5965fa8228edeac51576299182c2e31c", size = 516339 }, + { url = "https://files.pythonhosted.org/packages/fc/ad/a75e603161e79b7110c647163d130872b271c6b28712c803c65d492100f7/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5cc58aac218826d054c7da7f95821eba94125d88be673ff44267bb89d12a5866", size = 416201 }, + { url = "https://files.pythonhosted.org/packages/b9/42/555b4ee17508beafac135c8b450816ace5a96194ce97fefc49d58e5652ea/rpds_py-0.29.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de73e40ebc04dd5d9556f50180395322193a78ec247e637e741c1b954810f295", size = 395095 }, + { url = "https://files.pythonhosted.org/packages/cd/f0/c90b671b9031e800ec45112be42ea9f027f94f9ac25faaac8770596a16a1/rpds_py-0.29.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:295ce5ac7f0cf69a651ea75c8f76d02a31f98e5698e82a50a5f4d4982fbbae3b", size = 410077 }, + { url = "https://files.pythonhosted.org/packages/3d/80/9af8b640b81fe21e6f718e9dec36c0b5f670332747243130a5490f292245/rpds_py-0.29.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ea59b23ea931d494459c8338056fe7d93458c0bf3ecc061cd03916505369d55", size = 424548 }, + { url = "https://files.pythonhosted.org/packages/e4/0b/b5647446e991736e6a495ef510e6710df91e880575a586e763baeb0aa770/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f49d41559cebd608042fdcf54ba597a4a7555b49ad5c1c0c03e0af82692661cd", size = 573661 }, + { url = "https://files.pythonhosted.org/packages/f7/b3/1b1c9576839ff583d1428efbf59f9ee70498d8ce6c0b328ac02f1e470879/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:05a2bd42768ea988294ca328206efbcc66e220d2d9b7836ee5712c07ad6340ea", size = 600937 }, + { url = "https://files.pythonhosted.org/packages/6c/7b/b6cfca2f9fee4c4494ce54f7fb1b9f578867495a9aa9fc0d44f5f735c8e0/rpds_py-0.29.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33ca7bdfedd83339ca55da3a5e1527ee5870d4b8369456b5777b197756f3ca22", size = 564496 }, + { url = "https://files.pythonhosted.org/packages/b9/fb/ba29ec7f0f06eb801bac5a23057a9ff7670623b5e8013bd59bec4aa09de8/rpds_py-0.29.0-cp313-cp313-win32.whl", hash = "sha256:20c51ae86a0bb9accc9ad4e6cdeec58d5ebb7f1b09dd4466331fc65e1766aae7", size = 223126 }, + { url = "https://files.pythonhosted.org/packages/3c/6b/0229d3bed4ddaa409e6d90b0ae967ed4380e4bdd0dad6e59b92c17d42457/rpds_py-0.29.0-cp313-cp313-win_amd64.whl", hash = "sha256:6410e66f02803600edb0b1889541f4b5cc298a5ccda0ad789cc50ef23b54813e", size = 239771 }, + { url = "https://files.pythonhosted.org/packages/e4/38/d2868f058b164f8efd89754d85d7b1c08b454f5c07ac2e6cc2e9bd4bd05b/rpds_py-0.29.0-cp313-cp313-win_arm64.whl", hash = "sha256:56838e1cd9174dc23c5691ee29f1d1be9eab357f27efef6bded1328b23e1ced2", size = 229994 }, + { url = "https://files.pythonhosted.org/packages/52/91/5de91c5ec7d41759beec9b251630824dbb8e32d20c3756da1a9a9d309709/rpds_py-0.29.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:37d94eadf764d16b9a04307f2ab1d7af6dc28774bbe0535c9323101e14877b4c", size = 365886 }, + { url = "https://files.pythonhosted.org/packages/85/7c/415d8c1b016d5f47ecec5145d9d6d21002d39dce8761b30f6c88810b455a/rpds_py-0.29.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d472cf73efe5726a067dce63eebe8215b14beabea7c12606fd9994267b3cfe2b", size = 355262 }, + { url = "https://files.pythonhosted.org/packages/3d/14/bf83e2daa4f980e4dc848aed9299792a8b84af95e12541d9e7562f84a6ef/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:72fdfd5ff8992e4636621826371e3ac5f3e3b8323e9d0e48378e9c13c3dac9d0", size = 384826 }, + { url = "https://files.pythonhosted.org/packages/33/b8/53330c50a810ae22b4fbba5e6cf961b68b9d72d9bd6780a7c0a79b070857/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2549d833abdf8275c901313b9e8ff8fba57e50f6a495035a2a4e30621a2f7cc4", size = 394234 }, + { url = "https://files.pythonhosted.org/packages/cc/32/01e2e9645cef0e584f518cfde4567563e57db2257244632b603f61b40e50/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4448dad428f28a6a767c3e3b80cde3446a22a0efbddaa2360f4bb4dc836d0688", size = 520008 }, + { url = "https://files.pythonhosted.org/packages/98/c3/0d1b95a81affae2b10f950782e33a1fd2edd6ce2a479966cac98c9a66f57/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:115f48170fd4296a33938d8c11f697f5f26e0472e43d28f35624764173a60e4d", size = 409569 }, + { url = "https://files.pythonhosted.org/packages/fa/60/aa3b8678f3f009f675b99174fa2754302a7fbfe749162e8043d111de2d88/rpds_py-0.29.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e5bb73ffc029820f4348e9b66b3027493ae00bca6629129cd433fd7a76308ee", size = 385188 }, + { url = "https://files.pythonhosted.org/packages/92/02/5546c1c8aa89c18d40c1fcffdcc957ba730dee53fb7c3ca3a46f114761d2/rpds_py-0.29.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:b1581fcde18fcdf42ea2403a16a6b646f8eb1e58d7f90a0ce693da441f76942e", size = 398587 }, + { url = "https://files.pythonhosted.org/packages/6c/e0/ad6eeaf47e236eba052fa34c4073078b9e092bd44da6bbb35aaae9580669/rpds_py-0.29.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16e9da2bda9eb17ea318b4c335ec9ac1818e88922cbe03a5743ea0da9ecf74fb", size = 416641 }, + { url = "https://files.pythonhosted.org/packages/1a/93/0acedfd50ad9cdd3879c615a6dc8c5f1ce78d2fdf8b87727468bb5bb4077/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:28fd300326dd21198f311534bdb6d7e989dd09b3418b3a91d54a0f384c700967", size = 566683 }, + { url = "https://files.pythonhosted.org/packages/62/53/8c64e0f340a9e801459fc6456821abc15b3582cb5dc3932d48705a9d9ac7/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2aba991e041d031c7939e1358f583ae405a7bf04804ca806b97a5c0e0af1ea5e", size = 592730 }, + { url = "https://files.pythonhosted.org/packages/85/ef/3109b6584f8c4b0d2490747c916df833c127ecfa82be04d9a40a376f2090/rpds_py-0.29.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f437026dbbc3f08c99cc41a5b2570c6e1a1ddbe48ab19a9b814254128d4ea7a", size = 557361 }, + { url = "https://files.pythonhosted.org/packages/ff/3b/61586475e82d57f01da2c16edb9115a618afe00ce86fe1b58936880b15af/rpds_py-0.29.0-cp313-cp313t-win32.whl", hash = "sha256:6e97846e9800a5d0fe7be4d008f0c93d0feeb2700da7b1f7528dabafb31dfadb", size = 211227 }, + { url = "https://files.pythonhosted.org/packages/3b/3a/12dc43f13594a54ea0c9d7e9d43002116557330e3ad45bc56097ddf266e2/rpds_py-0.29.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f49196aec7c4b406495f60e6f947ad71f317a765f956d74bbd83996b9edc0352", size = 225248 }, + { url = "https://files.pythonhosted.org/packages/89/b1/0b1474e7899371d9540d3bbb2a499a3427ae1fc39c998563fe9035a1073b/rpds_py-0.29.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:394d27e4453d3b4d82bb85665dc1fcf4b0badc30fc84282defed71643b50e1a1", size = 363731 }, + { url = "https://files.pythonhosted.org/packages/28/12/3b7cf2068d0a334ed1d7b385a9c3c8509f4c2bcba3d4648ea71369de0881/rpds_py-0.29.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55d827b2ae95425d3be9bc9a5838b6c29d664924f98146557f7715e331d06df8", size = 354343 }, + { url = "https://files.pythonhosted.org/packages/eb/73/5afcf8924bc02a749416eda64e17ac9c9b28f825f4737385295a0e99b0c1/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc31a07ed352e5462d3ee1b22e89285f4ce97d5266f6d1169da1142e78045626", size = 385406 }, + { url = "https://files.pythonhosted.org/packages/c8/37/5db736730662508535221737a21563591b6f43c77f2e388951c42f143242/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4695dd224212f6105db7ea62197144230b808d6b2bba52238906a2762f1d1e7", size = 396162 }, + { url = "https://files.pythonhosted.org/packages/70/0d/491c1017d14f62ce7bac07c32768d209a50ec567d76d9f383b4cfad19b80/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcae1770b401167f8b9e1e3f566562e6966ffa9ce63639916248a9e25fa8a244", size = 517719 }, + { url = "https://files.pythonhosted.org/packages/d7/25/b11132afcb17cd5d82db173f0c8dab270ffdfaba43e5ce7a591837ae9649/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90f30d15f45048448b8da21c41703b31c61119c06c216a1bf8c245812a0f0c17", size = 409498 }, + { url = "https://files.pythonhosted.org/packages/0f/7d/e6543cedfb2e6403a1845710a5ab0e0ccf8fc288e0b5af9a70bfe2c12053/rpds_py-0.29.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44a91e0ab77bdc0004b43261a4b8cd6d6b451e8d443754cfda830002b5745b32", size = 382743 }, + { url = "https://files.pythonhosted.org/packages/75/11/a4ebc9f654293ae9fefb83b2b6be7f3253e85ea42a5db2f77d50ad19aaeb/rpds_py-0.29.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:4aa195e5804d32c682e453b34474f411ca108e4291c6a0f824ebdc30a91c973c", size = 400317 }, + { url = "https://files.pythonhosted.org/packages/52/18/97677a60a81c7f0e5f64e51fb3f8271c5c8fcabf3a2df18e97af53d7c2bf/rpds_py-0.29.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7971bdb7bf4ee0f7e6f67fa4c7fbc6019d9850cc977d126904392d363f6f8318", size = 416979 }, + { url = "https://files.pythonhosted.org/packages/f0/69/28ab391a9968f6c746b2a2db181eaa4d16afaa859fedc9c2f682d19f7e18/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8ae33ad9ce580c7a47452c3b3f7d8a9095ef6208e0a0c7e4e2384f9fc5bf8212", size = 567288 }, + { url = "https://files.pythonhosted.org/packages/3b/d3/0c7afdcdb830eee94f5611b64e71354ffe6ac8df82d00c2faf2bfffd1d4e/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:c661132ab2fb4eeede2ef69670fd60da5235209874d001a98f1542f31f2a8a94", size = 593157 }, + { url = "https://files.pythonhosted.org/packages/e2/ac/a0fcbc2feed4241cf26d32268c195eb88ddd4bd862adfc9d4b25edfba535/rpds_py-0.29.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bb78b3a0d31ac1bde132c67015a809948db751cb4e92cdb3f0b242e430b6ed0d", size = 554741 }, + { url = "https://files.pythonhosted.org/packages/0f/f1/fcc24137c470df8588674a677f33719d5800ec053aaacd1de8a5d5d84d9e/rpds_py-0.29.0-cp314-cp314-win32.whl", hash = "sha256:f475f103488312e9bd4000bc890a95955a07b2d0b6e8884aef4be56132adbbf1", size = 215508 }, + { url = "https://files.pythonhosted.org/packages/7b/c7/1d169b2045512eac019918fc1021ea07c30e84a4343f9f344e3e0aa8c788/rpds_py-0.29.0-cp314-cp314-win_amd64.whl", hash = "sha256:b9cf2359a4fca87cfb6801fae83a76aedf66ee1254a7a151f1341632acf67f1b", size = 228125 }, + { url = "https://files.pythonhosted.org/packages/be/36/0cec88aaba70ec4a6e381c444b0d916738497d27f0c30406e3d9fcbd3bc2/rpds_py-0.29.0-cp314-cp314-win_arm64.whl", hash = "sha256:9ba8028597e824854f0f1733d8b964e914ae3003b22a10c2c664cb6927e0feb9", size = 221992 }, + { url = "https://files.pythonhosted.org/packages/b1/fa/a2e524631717c9c0eb5d90d30f648cfba6b731047821c994acacb618406c/rpds_py-0.29.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:e71136fd0612556b35c575dc2726ae04a1669e6a6c378f2240312cf5d1a2ab10", size = 366425 }, + { url = "https://files.pythonhosted.org/packages/a2/a4/6d43ebe0746ff694a30233f63f454aed1677bd50ab7a59ff6b2bb5ac61f2/rpds_py-0.29.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:76fe96632d53f3bf0ea31ede2f53bbe3540cc2736d4aec3b3801b0458499ef3a", size = 355282 }, + { url = "https://files.pythonhosted.org/packages/fa/a7/52fd8270e0320b09eaf295766ae81dd175f65394687906709b3e75c71d06/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9459a33f077130dbb2c7c3cea72ee9932271fb3126404ba2a2661e4fe9eb7b79", size = 384968 }, + { url = "https://files.pythonhosted.org/packages/f4/7d/e6bc526b7a14e1ef80579a52c1d4ad39260a058a51d66c6039035d14db9d/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c9546cfdd5d45e562cc0444b6dddc191e625c62e866bf567a2c69487c7ad28a", size = 394714 }, + { url = "https://files.pythonhosted.org/packages/c0/3f/f0ade3954e7db95c791e7eaf978aa7e08a756d2046e8bdd04d08146ed188/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12597d11d97b8f7e376c88929a6e17acb980e234547c92992f9f7c058f1a7310", size = 520136 }, + { url = "https://files.pythonhosted.org/packages/87/b3/07122ead1b97009715ab9d4082be6d9bd9546099b2b03fae37c3116f72be/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28de03cf48b8a9e6ec10318f2197b83946ed91e2891f651a109611be4106ac4b", size = 409250 }, + { url = "https://files.pythonhosted.org/packages/c9/c6/dcbee61fd1dc892aedcb1b489ba661313101aa82ec84b1a015d4c63ebfda/rpds_py-0.29.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7951c964069039acc9d67a8ff1f0a7f34845ae180ca542b17dc1456b1f1808", size = 384940 }, + { url = "https://files.pythonhosted.org/packages/47/11/914ecb6f3574cf9bf8b38aced4063e0f787d6e1eb30b181a7efbc6c1da9a/rpds_py-0.29.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:c07d107b7316088f1ac0177a7661ca0c6670d443f6fe72e836069025e6266761", size = 399392 }, + { url = "https://files.pythonhosted.org/packages/f5/fd/2f4bd9433f58f816434bb934313584caa47dbc6f03ce5484df8ac8980561/rpds_py-0.29.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de2345af363d25696969befc0c1688a6cb5e8b1d32b515ef84fc245c6cddba3", size = 416796 }, + { url = "https://files.pythonhosted.org/packages/79/a5/449f0281af33efa29d5c71014399d74842342ae908d8cd38260320167692/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:00e56b12d2199ca96068057e1ae7f9998ab6e99cda82431afafd32f3ec98cca9", size = 566843 }, + { url = "https://files.pythonhosted.org/packages/ab/32/0a6a1ccee2e37fcb1b7ba9afde762b77182dbb57937352a729c6cd3cf2bb/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3919a3bbecee589300ed25000b6944174e07cd20db70552159207b3f4bbb45b8", size = 593956 }, + { url = "https://files.pythonhosted.org/packages/4a/3d/eb820f95dce4306f07a495ede02fb61bef36ea201d9137d4fcd5ab94ec1e/rpds_py-0.29.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7fa2ccc312bbd91e43aa5e0869e46bc03278a3dddb8d58833150a18b0f0283a", size = 557288 }, + { url = "https://files.pythonhosted.org/packages/e9/f8/b8ff786f40470462a252918e0836e0db903c28e88e3eec66bc4a7856ee5d/rpds_py-0.29.0-cp314-cp314t-win32.whl", hash = "sha256:97c817863ffc397f1e6a6e9d2d89fe5408c0a9922dac0329672fb0f35c867ea5", size = 211382 }, + { url = "https://files.pythonhosted.org/packages/c9/7f/1a65ae870bc9d0576aebb0c501ea5dccf1ae2178fe2821042150ebd2e707/rpds_py-0.29.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2023473f444752f0f82a58dfcbee040d0a1b3d1b3c2ec40e884bd25db6d117d2", size = 225919 }, + { url = "https://files.pythonhosted.org/packages/f2/ac/b97e80bf107159e5b9ba9c91df1ab95f69e5e41b435f27bdd737f0d583ac/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:acd82a9e39082dc5f4492d15a6b6c8599aa21db5c35aaf7d6889aea16502c07d", size = 373963 }, + { url = "https://files.pythonhosted.org/packages/40/5a/55e72962d5d29bd912f40c594e68880d3c7a52774b0f75542775f9250712/rpds_py-0.29.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:715b67eac317bf1c7657508170a3e011a1ea6ccb1c9d5f296e20ba14196be6b3", size = 364644 }, + { url = "https://files.pythonhosted.org/packages/99/2a/6b6524d0191b7fc1351c3c0840baac42250515afb48ae40c7ed15499a6a2/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3b1b87a237cb2dba4db18bcfaaa44ba4cd5936b91121b62292ff21df577fc43", size = 393847 }, + { url = "https://files.pythonhosted.org/packages/1c/b8/c5692a7df577b3c0c7faed7ac01ee3c608b81750fc5d89f84529229b6873/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c3c3e8101bb06e337c88eb0c0ede3187131f19d97d43ea0e1c5407ea74c0cbf", size = 407281 }, + { url = "https://files.pythonhosted.org/packages/f0/57/0546c6f84031b7ea08b76646a8e33e45607cc6bd879ff1917dc077bb881e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b8e54d6e61f3ecd3abe032065ce83ea63417a24f437e4a3d73d2f85ce7b7cfe", size = 529213 }, + { url = "https://files.pythonhosted.org/packages/fa/c1/01dd5f444233605555bc11fe5fed6a5c18f379f02013870c176c8e630a23/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3fbd4e9aebf110473a420dea85a238b254cf8a15acb04b22a5a6b5ce8925b760", size = 413808 }, + { url = "https://files.pythonhosted.org/packages/aa/0a/60f98b06156ea2a7af849fb148e00fbcfdb540909a5174a5ed10c93745c7/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fdf53d36e6c72819993e35d1ebeeb8e8fc688d0c6c2b391b55e335b3afba5a", size = 394600 }, + { url = "https://files.pythonhosted.org/packages/37/f1/dc9312fc9bec040ece08396429f2bd9e0977924ba7a11c5ad7056428465e/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:ea7173df5d86f625f8dde6d5929629ad811ed8decda3b60ae603903839ac9ac0", size = 408634 }, + { url = "https://files.pythonhosted.org/packages/ed/41/65024c9fd40c89bb7d604cf73beda4cbdbcebe92d8765345dd65855b6449/rpds_py-0.29.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:76054d540061eda273274f3d13a21a4abdde90e13eaefdc205db37c05230efce", size = 426064 }, + { url = "https://files.pythonhosted.org/packages/a2/e0/cf95478881fc88ca2fdbf56381d7df36567cccc39a05394beac72182cd62/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:9f84c549746a5be3bc7415830747a3a0312573afc9f95785eb35228bb17742ec", size = 575871 }, + { url = "https://files.pythonhosted.org/packages/ea/c0/df88097e64339a0218b57bd5f9ca49898e4c394db756c67fccc64add850a/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:0ea962671af5cb9a260489e311fa22b2e97103e3f9f0caaea6f81390af96a9ed", size = 601702 }, + { url = "https://files.pythonhosted.org/packages/87/f4/09ffb3ebd0cbb9e2c7c9b84d252557ecf434cd71584ee1e32f66013824df/rpds_py-0.29.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f7728653900035fb7b8d06e1e5900545d8088efc9d5d4545782da7df03ec803f", size = 564054 }, +] + +[[package]] +name = "safehttpx" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/d1/4282284d9cf1ee873607a46442da977fc3c985059315ab23610be31d5885/safehttpx-0.1.7.tar.gz", hash = "sha256:db201c0978c41eddb8bb480f3eee59dd67304fdd91646035e9d9a720049a9d23", size = 10385 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959 }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552 }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, +] + +[[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 = "sse-starlette" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765 }, +] + +[[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 = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901 }, +] + +[[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 = "typer" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8f/28/7c85c8032b91dbe79725b6f17d2fffc595dff06a35c7a30a37bef73a1ab4/typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37", size = 106492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028 }, +] + +[[package]] +name = "typer-slim" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/45/81b94a52caed434b94da65729c03ad0fb7665fab0f7db9ee54c94e541403/typer_slim-0.20.0.tar.gz", hash = "sha256:9fc6607b3c6c20f5c33ea9590cbeb17848667c51feee27d9e314a579ab07d1a3", size = 106561 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/dd/5cbf31f402f1cc0ab087c94d4669cfa55bd1e818688b910631e131d74e75/typer_slim-0.20.0-py3-none-any.whl", hash = "sha256:f42a9b7571a12b97dddf364745d29f12221865acef7a2680065f9bb29c7dc89d", size = 47087 }, +] + +[[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.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795 }, +] + +[[package]] +name = "uvicorn" +version = "0.38.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109 }, +] diff --git a/biome.json b/biome.json new file mode 100644 index 0000000000000000000000000000000000000000..eb3ea5402acd9034b665a4154bdfc9e7e0f3d89c --- /dev/null +++ b/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "complexity": { + "noForEach": "off", + "noUselessFragments": "off" + }, + "correctness": { + "useExhaustiveDependencies": "off" + }, + "suspicious": { + "noExplicitAny": "off" + }, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } + } + }, + "formatter": { + "enabled": true, + "includes": [ + "**", + "!**/node_modules/**/*", + "!**/*.config.*", + "!**/*.json", + "!**/tsconfig.json", + "!**/.turbo" + ] + } +} diff --git a/modal/README.md b/modal/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6ec654cb66573369812ff1eb4a65165fa2d65545 --- /dev/null +++ b/modal/README.md @@ -0,0 +1,237 @@ +# Modal Deployment for GPT-OSS vLLM + +Deploy OpenAI's GPT-OSS models (20B or 120B) on [Modal.com](https://modal.com) with vLLM for efficient inference. + +## 🚀 Quick Start + +### 1. Install Modal CLI + +```bash +# Install the Modal Python package +pip install modal + +# Authenticate with Modal (opens browser) +modal setup +``` + +If `modal setup` doesn't work, try: +```bash +python -m modal setup +``` + +### 2. Create a Modal Account + +1. Go to [modal.com](https://modal.com) +2. Create a free account +3. Run `modal setup` to authenticate + +### 3. Deploy the GPT-OSS Model + +```bash +# Navigate to the modal directory +cd modal + +# Test the server (spins up a temporary instance) +modal run gpt_oss_inference.py + +# Deploy to production (creates a persistent endpoint) +modal deploy gpt_oss_inference.py +``` + +## 📋 Configuration + +### GPU Selection (Cost Optimization) + +Edit `gpt_oss_inference.py` to choose your GPU tier: + +```python +# Choose your GPU - uncomment the one you want: +GPU_CONFIG = "A10G" # ~$0.76/hr - RECOMMENDED for budget ✅ +# GPU_CONFIG = "L4" # ~$0.59/hr - Cheapest option +# GPU_CONFIG = "A100" # ~$1.79/hr - More headroom +# GPU_CONFIG = "H100" # ~$3.95/hr - Maximum performance +``` + +### GPU Pricing Comparison + +| GPU | VRAM | Price/hr | Best For | +| --------- | ---- | ---------- | -------------------------------- | +| L4 | 24GB | ~$0.59 | Tightest budget (may be tight) | +| **A10G** | 24GB | **~$0.76** | **Best value for GPT-OSS 20B** ✅ | +| A100 40GB | 40GB | ~$1.79 | More headroom | +| A100 80GB | 80GB | ~$2.78 | Both 20B and 120B | +| H100 | 80GB | ~$3.95 | Maximum performance | + +### Model Selection + +```python +# 20B model - faster, fits on A10G/L4 +MODEL_NAME = "openai/gpt-oss-20b" + +# 120B model - needs A100 80GB or H100 +MODEL_NAME = "openai/gpt-oss-120b" +``` + +### Performance Tuning + +```python +# FAST_BOOT = True - Faster startup, less memory (use for smaller GPUs) +# FAST_BOOT = False - Slower startup, faster inference +FAST_BOOT = True + +# Data type - GPT-OSS MXFP4 quantization REQUIRES bfloat16 (float16 not supported) +# The Marlin kernel warning on A10G/L4 is expected and can be ignored +USE_FLOAT16 = False # Must be False for GPT-OSS (MXFP4 only supports bfloat16) + +# Maximum model length (context window) - reduce to speed up startup +MAX_MODEL_LEN = 32768 # 32k tokens (can increase to 131072 if needed) + +# Keep container warm longer to avoid cold starts +SCALEDOWN_WINDOW = 5 * MINUTES # Reduced from 10 minutes for faster warm starts + +# Maximum concurrent requests (reduce for smaller GPUs) +MAX_INPUTS = 50 +``` + +#### Startup Time Optimization + +The following optimizations are enabled by default to reduce the ~1 minute startup time: + +- **`--max-model-len 65536`**: Limits context window to 64k tokens (faster startup, can increase to 131072 if needed) +- **`--disable-custom-all-reduce`**: Disabled for single GPU (reduces startup overhead) +- **`--enable-prefix-caching`**: Enables prefix caching for faster subsequent requests +- **`--load-format auto`**: Auto-detects best loading format for faster model loading +- **Reduced scaledown window**: Keeps container warm for 5 minutes instead of 10 (faster warm starts) + +Note: `--dtype bfloat16` is required for GPT-OSS (MXFP4 quantization only supports bf16) + +## 🔧 Commands + +| Command | Description | +| --------------------------------------- | ---------------------------- | +| `modal run gpt_oss_inference.py` | Test with a temporary server | +| `modal deploy gpt_oss_inference.py` | Deploy to production | +| `modal app stop gpt-oss-vllm-inference` | Stop the deployed app | +| `modal app logs gpt-oss-vllm-inference` | View deployment logs | +| `modal volume ls` | List cached volumes | + +## 🌐 API Usage + +Once deployed, the server exposes an OpenAI-compatible API: + +### Endpoint URL + +After deployment, Modal will provide a URL like: +``` +https://your-workspace--gpt-oss-vllm-inference-serve.modal.run +``` + +### Making Requests + +```python +import openai + +client = openai.OpenAI( + base_url="https://your-workspace--gpt-oss-vllm-inference-serve.modal.run/v1", + api_key="not-needed" # Modal handles auth via the URL +) + +response = client.chat.completions.create( + model="llm", + messages=[ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "Hello!"} + ] +) +print(response.choices[0].message.content) +``` + +### cURL Example + +```bash +curl -X POST "https://your-workspace--gpt-oss-vllm-inference-serve.modal.run/v1/chat/completions" \ + -H "Content-Type: application/json" \ + -d '{ + "model": "llm", + "messages": [ + {"role": "user", "content": "Hello!"} + ] + }' +``` + +## 💰 Pricing + +Modal charges per second of usage: +- **A10G GPU**: ~$0.76/hour (recommended) ✅ +- **L4 GPU**: ~$0.59/hour (cheapest) +- **A100 40GB**: ~$1.79/hour +- **H100 GPU**: ~$3.95/hour (fastest) +- No charges when idle (scale to zero) +- First $30/month is free + +## 📦 Model Details + +### GPT-OSS 20B +- MoE architecture with efficient inference +- MXFP4 quantization for MoE layers (~10-15GB VRAM) +- Attention sink support for longer contexts +- **Fits on A10G, L4, A100, or H100** ✅ + +### GPT-OSS 120B +- Larger model with more capabilities +- Same quantization and architecture (~40-50GB VRAM) +- **Requires A100 80GB or H100** + +## 🔍 Troubleshooting + +### Authentication Issues +```bash +# Re-authenticate +modal token new +``` + +### GPU Availability +If your selected GPU is not available, Modal will queue your request. Tips: +- **A10G and L4** typically have better availability than H100 +- Try different regions +- Use off-peak hours +- Change `GPU_CONFIG` to a different tier + +### Marlin Kernel Warning +If you see: `You are running Marlin kernel with bf16 on GPUs before SM90`: +- **This warning can be safely ignored** - GPT-OSS uses MXFP4 quantization which **requires bfloat16** +- float16 is NOT supported for MXFP4 quantization (will cause a validation error) +- The warning is just a performance suggestion, but we cannot use fp16 for this model +- For optimal performance, use H100 (SM90+) which is optimized for bf16 + +### Startup Time Optimization +If startup takes ~1 minute: +- ✅ **Already optimized** - The code includes several optimizations: + - Uses `float16` instead of `bfloat16` for faster loading + - Limits context window to 32k tokens (faster memory allocation) + - Disables custom all-reduce for single GPU + - Enables prefix caching + - Uses auto load format detection +- To reduce startup further, you can: + - Increase `SCALEDOWN_WINDOW` to keep container warm longer (costs more) + - Use a larger GPU (A100/H100) for faster model loading + - Reduce `MAX_MODEL_LEN` if you don't need full context window + +### Cache Issues +```bash +# Clear vLLM cache +modal volume rm vllm-cache +modal volume create vllm-cache + +# Clear HuggingFace cache +modal volume rm huggingface-cache +modal volume create huggingface-cache +``` + +## 📚 Resources + +- [Modal Documentation](https://modal.com/docs/guide) +- [vLLM Documentation](https://docs.vllm.ai/) +- [GPT-OSS on HuggingFace](https://huggingface.co/openai/gpt-oss-20b) +- [Modal Examples](https://modal.com/docs/examples) + diff --git a/modal/deploy.sh b/modal/deploy.sh new file mode 100755 index 0000000000000000000000000000000000000000..f229e2d48707694af8b69eecdb39ae6ab3d62f12 --- /dev/null +++ b/modal/deploy.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# ============================================================================= +# Modal GPT-OSS Deployment Script +# ============================================================================= + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "🚀 Modal GPT-OSS vLLM Deployment" +echo "================================" +echo "" + +# Check if modal is installed +if ! command -v modal &> /dev/null; then + echo "❌ Modal CLI not found. Installing..." + pip install modal + echo "" +fi + +# Check if authenticated +echo "📋 Checking Modal authentication..." +if ! modal token info &> /dev/null 2>&1; then + echo "❌ Not authenticated with Modal." + echo "" + echo "Please run: modal setup" + echo "Or set your token with: modal token set --token-id --token-secret " + echo "" + exit 1 +fi + +echo "✅ Authenticated with Modal" +echo "" + +# Show options +echo "What would you like to do?" +echo "" +echo " 1) Test the server (temporary deployment)" +echo " 2) Deploy to production" +echo " 3) Stop the deployed app" +echo " 4) View logs" +echo " 5) Exit" +echo "" +read -p "Enter choice [1-5]: " choice + +case $choice in + 1) + echo "" + echo "🧪 Running test deployment..." + cd "$SCRIPT_DIR" + modal run gpt_oss_inference.py + ;; + 2) + echo "" + echo "🚀 Deploying to production..." + cd "$SCRIPT_DIR" + modal deploy gpt_oss_inference.py + echo "" + echo "✅ Deployment complete!" + echo "" + echo "Your endpoint URL will be displayed above." + ;; + 3) + echo "" + echo "🛑 Stopping app..." + modal app stop gpt-oss-vllm-inference + echo "✅ App stopped" + ;; + 4) + echo "" + echo "📜 Fetching logs..." + modal app logs gpt-oss-vllm-inference + ;; + 5) + echo "👋 Goodbye!" + exit 0 + ;; + *) + echo "❌ Invalid choice" + exit 1 + ;; +esac + diff --git a/modal/gpt_oss_inference.py b/modal/gpt_oss_inference.py new file mode 100644 index 0000000000000000000000000000000000000000..dba3897b6485420d7b2e24b61cb4501131ff8453 --- /dev/null +++ b/modal/gpt_oss_inference.py @@ -0,0 +1,362 @@ +""" +GPT-OSS Model Deployment on Modal with vLLM + +This script deploys OpenAI's GPT-OSS models (20B or 120B) on Modal.com +with vLLM for efficient inference. + +Usage: + # First time setup - pre-download model weights (run once, takes ~5-10 min) + modal run gpt_oss_inference.py::download_model + + # Test the server locally + modal run gpt_oss_inference.py + + # Deploy to production + modal deploy gpt_oss_inference.py + +Performance Tips: + 1. Run download_model first to cache weights in the volume + 2. Reduce MAX_MODEL_LEN for faster startup (8k is sufficient for most use cases) + 3. Keep FAST_BOOT=True for cheaper GPUs (A10G, L4) + 4. Increase SCALEDOWN_WINDOW to reduce cold starts during demos + +Based on: https://modal.com/docs/examples/gpt_oss_inference +""" + +import json +import time +from datetime import datetime, timezone +from typing import Any + +import aiohttp +import modal + +# ============================================================================= +# Container Image Configuration +# ============================================================================= + +# Enable HF Transfer for faster model downloads (5-10x faster) +vllm_image = ( + modal.Image.from_registry( + "nvidia/cuda:12.8.1-devel-ubuntu22.04", + add_python="3.12", + ) + .entrypoint([]) + .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"}) # Enable fast downloads + .uv_pip_install( + "vllm==0.11.0", + "huggingface_hub[hf_transfer]==0.35.0", + "flashinfer-python==0.3.1", + ) +) + +# ============================================================================= +# Model Configuration +# ============================================================================= + +# Choose the model size - 20B is faster, 120B has more capabilities +MODEL_NAME = "openai/gpt-oss-20b" # or "openai/gpt-oss-120b" +MODEL_REVISION = "d666cf3b67006cf8227666739edf25164aaffdeb" + +# ============================================================================= +# GPU Configuration - CHOOSE YOUR GPU TIER +# ============================================================================= +# +# Modal GPU Pricing (approximate, per hour): +# ┌─────────────┬──────────┬────────────────────────────────────────────┐ +# │ GPU │ Price/hr │ Notes │ +# ├─────────────┼──────────┼────────────────────────────────────────────┤ +# │ T4 (16GB) │ ~$0.25 │ ❌ Too small for GPT-OSS │ +# │ L4 (24GB) │ ~$0.59 │ ⚠️ Tight fit, may work with 20B │ +# │ A10G (24GB) │ ~$0.76 │ ✅ Good balance for 20B model │ +# │ A100 40GB │ ~$1.79 │ ✅ Comfortable for 20B │ +# │ A100 80GB │ ~$2.78 │ ✅ Works for both 20B and 120B │ +# │ H100 (80GB) │ ~$3.95 │ ✅ Best performance, both models │ +# └─────────────┴──────────┴────────────────────────────────────────────┘ +# +# GPT-OSS 20B with MXFP4 quantization needs ~10-15GB VRAM +# GPT-OSS 120B needs ~40-50GB VRAM + +# Choose your GPU - uncomment the one you want to use: +GPU_CONFIG = "A100-40GB" # ~$0.76/hr - RECOMMENDED for budget (works with 20B) +# GPU_CONFIG = "L4" # ~$0.59/hr - Cheapest option (may be tight) +# GPU_CONFIG = "A100" # ~$1.79/hr - More headroom (40GB version) +# GPU_CONFIG = "H100" # ~$3.95/hr - Maximum performance + +# ============================================================================= +# Volume Configuration for Caching +# ============================================================================= + +# Cache for HuggingFace model weights +hf_cache_vol = modal.Volume.from_name("huggingface-cache", create_if_missing=True) + +# Cache for vLLM compilation artifacts +vllm_cache_vol = modal.Volume.from_name("vllm-cache", create_if_missing=True) + +# ============================================================================= +# Performance Configuration +# ============================================================================= + +MINUTES = 60 # Helper constant + +# FAST_BOOT = True: Faster startup but slower inference +# FAST_BOOT = False: Slower startup but faster inference (recommended for production) +FAST_BOOT = True # Use True for cheaper GPUs to reduce startup memory + +# CUDA graph capture sizes for optimized inference +CUDA_GRAPH_CAPTURE_SIZES = [1, 2, 4, 8, 16, 24, 32] + +# Data type configuration +# NOTE: GPT-OSS uses MXFP4 quantization which REQUIRES bfloat16 - float16 is NOT supported +# The Marlin kernel warning on A10G/L4 is expected and can be ignored +USE_FLOAT16 = False # Must be False for GPT-OSS (MXFP4 only supports bfloat16) + +# Maximum model length (context window) - SIGNIFICANTLY REDUCED for faster startup +# The KV cache allocation is proportional to context length, so smaller = much faster startup +# For EU AI Act assessments, 8k-16k tokens is more than enough +# GPT-OSS 20B supports up to 128k tokens, but we only need ~8k for our use case +MAX_MODEL_LEN = 16384 # 16k tokens - sufficient for compliance assessments, 4x faster startup + +# Server configuration +VLLM_PORT = 8000 +N_GPU = 1 # Number of GPUs for tensor parallelism +MAX_INPUTS = 50 # Reduced for smaller GPUs + +# Keep container warm longer to avoid cold starts (costs more but faster response) +# For hackathon demo: 10 minutes to reduce cold starts during presentation +SCALEDOWN_WINDOW = 10 * MINUTES # Increased for demo stability + +# ============================================================================= +# Modal App Definition +# ============================================================================= + +app = modal.App("gpt-oss-vllm-inference") + + +# Select GPU based on GPU_CONFIG +_GPU_MAP = { + "T4": "T4", + "L4": "L4", + "A10G": "A10G", + "A100": "A100:40GB", + "A100-80GB": "A100:80GB", + "H100": "H100", +} +SELECTED_GPU = _GPU_MAP.get(GPU_CONFIG, "A10G") + + +# ============================================================================= +# Pre-download Model Weights (reduces warm start time significantly) +# ============================================================================= + +@app.function( + image=vllm_image, + volumes={"/root/.cache/huggingface": hf_cache_vol}, + timeout=30 * MINUTES, +) +def download_model(): + """ + Pre-download the model weights to the volume cache. + Run this once with: modal run gpt_oss_inference.py::download_model + This will cache the weights and make subsequent starts much faster. + """ + from huggingface_hub import snapshot_download + + print(f"📥 Downloading model weights for {MODEL_NAME}...") + print(f" Revision: {MODEL_REVISION}") + + snapshot_download( + MODEL_NAME, + revision=MODEL_REVISION, + local_dir=f"/root/.cache/huggingface/hub/models--{MODEL_NAME.replace('/', '--')}", + ) + + print("✅ Model weights downloaded and cached!") + print(" Future container starts will use the cached weights.") + + +@app.function( + image=vllm_image, + gpu=SELECTED_GPU, + scaledown_window=SCALEDOWN_WINDOW, + timeout=30 * MINUTES, + volumes={ + "/root/.cache/huggingface": hf_cache_vol, + "/root/.cache/vllm": vllm_cache_vol, + }, +) +@modal.concurrent(max_inputs=MAX_INPUTS) +@modal.web_server(port=VLLM_PORT, startup_timeout=30 * MINUTES) +def serve(): + """Start the vLLM server with GPT-OSS model.""" + import subprocess + + cmd = [ + "vllm", + "serve", + "--uvicorn-log-level=info", + MODEL_NAME, + "--revision", + MODEL_REVISION, + "--served-model-name", + "llm", # Serve model as "llm" - this is what clients expect + "--host", + "0.0.0.0", + "--port", + str(VLLM_PORT), + ] + + # enforce-eager disables both Torch compilation and CUDA graph capture + # default is no-enforce-eager. see the --compilation-config flag for tighter control + cmd += ["--enforce-eager" if FAST_BOOT else "--no-enforce-eager"] + + if not FAST_BOOT: # CUDA graph capture is only used with `--no-enforce-eager` + cmd += [ + "-O.cudagraph_capture_sizes=" + + str(CUDA_GRAPH_CAPTURE_SIZES).replace(" ", "") + ] + + # Data type optimization: use float16 for A10G/L4 (SM86) to avoid Marlin kernel warning + # bf16 is optimized for SM90+ (H100), fp16 is better for Ampere architecture + if USE_FLOAT16: + cmd += ["--dtype", "float16"] + else: + cmd += ["--dtype", "bfloat16"] + + # Limit context length to speed up startup and reduce memory allocation + cmd += ["--max-model-len", str(MAX_MODEL_LEN)] + + # Disable custom all-reduce for single GPU (reduces startup overhead) + if N_GPU == 1: + cmd += ["--disable-custom-all-reduce"] + + # Enable prefix caching for faster subsequent requests + cmd += ["--enable-prefix-caching"] + + # Trust remote code for GPT-OSS models + cmd += ["--trust-remote-code"] + + # Optimize loading format for faster startup + cmd += ["--load-format", "auto"] # Auto-detect best format + + # assume multiple GPUs are for splitting up large matrix multiplications + cmd += ["--tensor-parallel-size", str(N_GPU)] + + # Additional optimizations for faster startup and inference + # Disable usage stats collection to speed up startup + cmd += ["--disable-log-stats"] + + # Use swap space if needed (helps with memory pressure on smaller GPUs) + cmd += ["--swap-space", "4"] # 4GB swap space + + print(f"Starting vLLM server with command: {' '.join(cmd)}") + + subprocess.Popen(" ".join(cmd), shell=True) + + +# ============================================================================= +# Local Test Entrypoint +# ============================================================================= + + +@app.local_entrypoint() +async def test(test_timeout=30 * MINUTES, user_content=None, twice=True): + """ + Test the deployed server with a sample prompt. + + Args: + test_timeout: Maximum time to wait for server health + user_content: Custom prompt to send (default: SVD explanation) + twice: Whether to send a second request + """ + url = serve.get_web_url() + + system_prompt = { + "role": "system", + "content": f"""You are ChatModal, a large language model trained by Modal. +Knowledge cutoff: 2024-06 +Current date: {datetime.now(timezone.utc).date()} +Reasoning: low +# Valid channels: analysis, commentary, final. Channel must be included for every message. +Calls to these tools must go to the commentary channel: 'functions'.""", + } + + if user_content is None: + user_content = "Explain what the Singular Value Decomposition is." + + messages = [ # OpenAI chat format + system_prompt, + {"role": "user", "content": user_content}, + ] + + async with aiohttp.ClientSession(base_url=url) as session: + print(f"Running health check for server at {url}") + async with session.get("/health", timeout=test_timeout - 1 * MINUTES) as resp: + up = resp.status == 200 + assert up, f"Failed health check for server at {url}" + print(f"Successful health check for server at {url}") + + print(f"Sending messages to {url}:", *messages, sep="\n\t") + await _send_request(session, "llm", messages) + + if twice: + messages[0]["content"] += "\nTalk like a pirate, matey." + print(f"Re-sending messages to {url}:", *messages, sep="\n\t") + await _send_request(session, "llm", messages) + + +async def _send_request( + session: aiohttp.ClientSession, model: str, messages: list +) -> None: + """Send a streaming request to the vLLM server.""" + # `stream=True` tells an OpenAI-compatible backend to stream chunks + payload: dict[str, Any] = {"messages": messages, "model": model, "stream": True} + + headers = {"Content-Type": "application/json", "Accept": "text/event-stream"} + + t = time.perf_counter() + async with session.post( + "/v1/chat/completions", json=payload, headers=headers, timeout=10 * MINUTES + ) as resp: + async for raw in resp.content: + resp.raise_for_status() + # extract new content and stream it + line = raw.decode().strip() + if not line or line == "data: [DONE]": + continue + if line.startswith("data: "): # SSE prefix + line = line[len("data: ") :] + + chunk = json.loads(line) + assert ( + chunk["object"] == "chat.completion.chunk" + ) # or something went horribly wrong + delta = chunk["choices"][0]["delta"] + + if "content" in delta: + print(delta["content"], end="") # print the content as it comes in + elif "reasoning_content" in delta: + print(delta["reasoning_content"], end="") + elif not delta: + print() + else: + raise ValueError(f"Unsupported response delta: {delta}") + print("") + print(f"Time to Last Token: {time.perf_counter() - t:.2f} seconds") + + +# ============================================================================= +# Utility Functions +# ============================================================================= + + +def get_endpoint_url() -> str: + """Get the deployed endpoint URL.""" + return serve.get_web_url() + + +if __name__ == "__main__": + print("Run this script with Modal:") + print(" modal run gpt_oss_inference.py # Test the server") + print(" modal deploy gpt_oss_inference.py # Deploy to production") + diff --git a/modal/requirements.txt b/modal/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..37c15922296f48b112ca8918e65d4ecd262c5628 --- /dev/null +++ b/modal/requirements.txt @@ -0,0 +1,4 @@ +# Modal CLI and dependencies +modal>=0.64.0 +aiohttp>=3.9.0 + diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..39798b21b99cc694f728d7d27a9ce2a5f8776768 --- /dev/null +++ b/package.json @@ -0,0 +1,40 @@ +{ + "name": "t3-turbo-biome", + "description": "A full stack turborepo template with biome and shadcn native and web ui.", + "private": true, + "engines": { + "node": ">=18.18.2" + }, + "packageManager": "pnpm@10.24.0", + "scripts": { + "build": "turbo build", + "clean": "git clean -xdf node_modules", + "clean:workspaces": "turbo clean", + "dev": "turbo dev --parallel", + "format": "turbo format --continue --", + "format:fix": "turbo format --continue -- --write", + "lint": "turbo lint --continue --", + "lint:fix": "turbo lint --continue -- --apply", + "typecheck": "turbo typecheck" + }, + "devDependencies": { + "@biomejs/biome": "^2.3.8", + "@turbo/gen": "^2.6.1", + "turbo": "^2.6.1", + "typescript": "^5.9.3" + }, + "pnpm": { + "overrides": { + "@auth/core": "0.18.0" + }, + "onlyBuiltDependencies": [ + "@biomejs/biome", + "core-js-pure", + "esbuild", + "sharp" + ] + }, + "dependencies": { + "dotenv": "^17.2.3" + } +} diff --git a/packages/eu-ai-act-mcp/README.md b/packages/eu-ai-act-mcp/README.md new file mode 100644 index 0000000000000000000000000000000000000000..4c331908779a4d8dab03f14e92e089578b03225d --- /dev/null +++ b/packages/eu-ai-act-mcp/README.md @@ -0,0 +1,674 @@ +# @eu-ai-act/mcp-server + +**MCP Server for EU AI Act Compliance** + +A Model Context Protocol (MCP) server providing organization discovery and AI systems inventory tools for EU AI Act compliance, based on Regulation (EU) 2024/1689. + +## Features + +- 🏢 **Organization Discovery** - Discover and profile organizations for compliance assessment +- 🤖 **AI Systems Discovery** - Inventory and classify AI systems according to EU AI Act risk tiers +- ⚖️ **Compliance Analysis** - Gap analysis with specific Article references from the AI Act +- 📊 **Risk Classification** - Automated risk categorization (Unacceptable, High, Limited, Minimal) +- 📝 **Documentation Status** - Track technical documentation and conformity assessment requirements +- 🧠 **Multi-Model AI Assessment** - 5 AI models supported for compliance assessment: + - Claude 4.5 Sonnet & Claude Opus 4 (Anthropic) + - GPT-5 (OpenAI) + - Grok 4.1 (xAI) + - Gemini 3 Pro (Google) +- 🔄 **Dynamic Model Selection** - Pass model as parameter to `assess_compliance` tool + +## Installation + +### Local Development + +```bash +# Clone the repository +git clone https://github.com/your-org/mcp-1st-birthday-ai-act.git +cd mcp-1st-birthday-ai-act + +# Install dependencies +pnpm install + +# Build the MCP server +pnpm --filter @eu-ai-act/mcp-server build + +# Run locally +pnpm --filter @eu-ai-act/mcp-server dev +``` + +### NPM Package (Coming Soon) + +```bash +npm install -g @eu-ai-act/mcp-server +``` + +## Tavily AI Integration + +This MCP server uses **Tavily AI** for intelligent company research and organization discovery. Tavily provides AI-powered web search optimized for LLMs and RAG systems. + +### Setting up Tavily + +1. **Get your Tavily API key** (free tier available): + - Visit [https://app.tavily.com](https://app.tavily.com) + - Sign up for a free account (1,000 API credits/month) + - Copy your API key + +2. **Configure the API keys**: + + Create a `.env` file in the project root: + ```bash + # Required: Tavily API key for web research + TAVILY_API_KEY=tvly-YOUR_API_KEY + + # Required: Choose one model and provide its API key + # For Claude 4.5 or Claude Opus: + ANTHROPIC_API_KEY=sk-ant-YOUR_KEY + # OR for GPT-5: + OPENAI_API_KEY=sk-YOUR_KEY + # OR for Grok 4.1: + XAI_API_KEY=xai-YOUR_KEY + # OR for Gemini 3 Pro: + GOOGLE_GENERATIVE_AI_API_KEY=AIza-YOUR_KEY + ``` + + Or set as environment variables: + ```bash + export TAVILY_API_KEY=tvly-YOUR_API_KEY + # Choose one model: + export ANTHROPIC_API_KEY=sk-ant-YOUR_KEY # For Claude 4.5 or Claude Opus + # OR + export OPENAI_API_KEY=sk-YOUR_KEY # For GPT-5 + # OR + export XAI_API_KEY=xai-YOUR_KEY # For Grok 4.1 + # OR + export GOOGLE_GENERATIVE_AI_API_KEY=AIza... # For Gemini 3 Pro + ``` + +3. **For Claude Desktop**, add the environment variables to your config: + + ```json + { + "mcpServers": { + "eu-ai-act": { + "command": "node", + "args": ["/path/to/packages/eu-ai-act-mcp/dist/index.js"], + "env": { + "TAVILY_API_KEY": "tvly-YOUR_API_KEY", + "ANTHROPIC_API_KEY": "sk-ant-YOUR_KEY", + "OPENAI_API_KEY": "sk-YOUR_KEY", + "XAI_API_KEY": "xai-YOUR_KEY", + "GOOGLE_GENERATIVE_AI_API_KEY": "AIza-YOUR_KEY" + } + } + } + } + ``` + + **Note**: You only need to set the API key for the model you want to use (Claude 4.5, Claude Opus, GPT-5, Grok 4.1, or Gemini 3 Pro). + +### How Tavily Enhances Organization Discovery + +The `discover_organization` tool uses Tavily to perform multi-step research: + +1. **Company Overview Search** - Discovers business model, products, services +2. **AI/Technology Search** - Identifies AI capabilities and maturity +3. **Compliance Search** - Finds existing certifications (GDPR, ISO 27001, etc.) + +Tavily's advanced search depth provides: +- ✅ AI-generated summaries of company information +- ✅ Reliable source citations from company websites +- ✅ Automatic extraction of key business details +- ✅ Real-time web research (not limited to training data) + +**Example with Tavily-powered discovery:** + +```typescript +// When TAVILY_API_KEY is set, you get real company research +const result = await client.callTool({ + name: "discover_organization", + arguments: { + organizationName: "OpenAI", + domain: "openai.com", + context: "AI research company" + } +}); + +// Result includes real research data: +// - Actual business sector and size +// - Real AI maturity assessment +// - Discovered certifications +// - Source URLs from Tavily +``` + +**Without Tavily**, the tool falls back to mock data for development purposes. + +### Tavily Resources + +- [Tavily Documentation](https://docs.tavily.com/) +- [Company Research Use Case](https://docs.tavily.com/examples/use-cases/company-research) +- [JavaScript SDK Reference](https://docs.tavily.com/sdk/javascript/reference) + +## Usage + +### With Claude Desktop + +Add to your Claude Desktop configuration (`claude_desktop_config.json`): + +```json +{ + "mcpServers": { + "eu-ai-act": { + "command": "npx", + "args": ["-y", "@eu-ai-act/mcp-server"] + } + } +} +``` + +Or for local development: + +```json +{ + "mcpServers": { + "eu-ai-act": { + "command": "node", + "args": ["/path/to/packages/eu-ai-act-mcp/dist/index.js"] + } + } +} +``` + +### With ChatGPT Apps SDK + +The MCP server is compatible with ChatGPT Apps SDK. Configuration example: + +1. **Set up connector** in ChatGPT Settings → Apps & Connectors → Advanced settings +2. **Add connector URL**: `https://your-server.com/mcp` (requires HTTP/SSE deployment) +3. **Test the connection** using the test agent + +For HTTP/SSE deployment, see the [ChatGPT App Deployment Guide](#chatgpt-app-deployment). + +### Programmatic Usage + +```typescript +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +const client = new Client({ name: "my-app", version: "1.0.0" }, { capabilities: {} }); +const transport = new StdioClientTransport({ + command: "node", + args: ["./node_modules/@eu-ai-act/mcp-server/dist/index.js"], +}); + +await client.connect(transport); + +// Discover organization +const orgResult = await client.callTool({ + name: "discover_organization", + arguments: { + organizationName: "Acme AI Solutions GmbH", + domain: "acme-ai.de", + }, +}); + +// Discover AI services +const servicesResult = await client.callTool({ + name: "discover_ai_services", + arguments: { + organizationContext: JSON.parse(orgResult.content[0].text), + scope: "all", + }, +}); +``` + +## Tools + +### 1. `discover_organization` + +Discovers and profiles an organization for EU AI Act compliance assessment. + +**Input:** +```json +{ + "organizationName": "Acme AI Solutions GmbH", + "domain": "acme-ai.de", + "context": "German AI startup focused on healthcare solutions" +} +``` + +**Output Schema:** +```typescript +{ + organization: { + name: string; + registrationNumber?: string; + sector: string; + size: "SME" | "Large Enterprise" | "Public Body" | "Micro Enterprise"; + jurisdiction: string[]; + euPresence: boolean; + headquarters: { country: string; city: string; address?: string }; + contact: { email: string; phone?: string; website?: string }; + aiMaturityLevel: "Nascent" | "Developing" | "Advanced" | "Expert"; + aiSystemsCount?: number; + primaryRole: "Provider" | "Deployer" | "Importer" | "Distributor" | "Authorized Representative"; + }; + regulatoryContext: { + applicableFrameworks: string[]; + complianceDeadlines: Array<{ date: string; description: string; article: string }>; + existingCertifications: string[]; + hasAuthorizedRepresentative?: boolean; + notifiedBodyId?: string; + hasQualityManagementSystem: boolean; + hasRiskManagementSystem: boolean; + }; + metadata: { + createdAt: string; + lastUpdated: string; + completenessScore: number; + dataSource: string; + }; +} +``` + +**EU AI Act References:** +- Article 16 (Provider Obligations) +- Article 17 (Quality Management System) +- Article 22 (Authorized Representatives) +- Article 49 (Registration) + +### 2. `discover_ai_services` + +Discovers and classifies AI systems within an organization according to EU AI Act requirements. + +**Input:** +```json +{ + "organizationContext": { /* from discover_organization */ }, + "systemNames": ["System A", "System B"], + "scope": "all" +} +``` + +**Output Schema:** +```typescript +{ + systems: Array<{ + system: { + name: string; + systemId?: string; + description: string; + intendedPurpose: string; + version: string; + status: "Development" | "Testing" | "Production" | "Deprecated"; + provider: { name: string; role: string; contact: string }; + }; + riskClassification: { + category: "Unacceptable" | "High" | "Limited" | "Minimal"; + annexIIICategory?: string; + justification: string; + safetyComponent: boolean; + riskScore: number; + conformityAssessmentRequired: boolean; + conformityAssessmentType: "Internal Control" | "Third Party Assessment" | "Not Required" | "Pending"; + }; + technicalDetails: { + aiTechnology: string[]; + dataProcessed: string[]; + processesSpecialCategoryData: boolean; + deploymentModel: "On-premise" | "Cloud" | "Hybrid" | "Edge" | "SaaS"; + vendor?: string; + trainingData?: { description: string; sources: string[]; biasAssessment: boolean }; + integrations: string[]; + humanOversight: { enabled: boolean; description?: string }; + }; + complianceStatus: { + hasTechnicalDocumentation: boolean; + conformityAssessmentStatus: "Not Started" | "In Progress" | "Completed" | "Not Required"; + hasEUDeclaration: boolean; + hasCEMarking: boolean; + registeredInEUDatabase: boolean; + hasPostMarketMonitoring: boolean; + hasAutomatedLogging: boolean; + lastAssessmentDate?: string; + identifiedGaps: string[]; + }; + metadata: { + createdAt: string; + lastUpdated: string; + dataSource: string; + discoveryMethod: string; + }; + }>; + riskSummary: { + unacceptableRiskCount: number; + highRiskCount: number; + limitedRiskCount: number; + minimalRiskCount: number; + totalCount: number; + }; + complianceSummary: { + fullyCompliantCount: number; + partiallyCompliantCount: number; + nonCompliantCount: number; + requiresAttention: Array; + }; + discoveryMetadata: { + timestamp: string; + method: string; + coverage: string; + }; +} +``` + +**EU AI Act References:** +- Article 6 (Classification Rules) +- Article 9 (Risk Management System) +- Article 10 (Data Governance) +- Article 11 (Technical Documentation) +- Article 12 (Record-Keeping) +- Article 14 (Human Oversight) +- Article 43 (Conformity Assessment) +- Article 47 (EU Declaration of Conformity) +- Article 48 (CE Marking) +- Article 49 (Registration) +- Article 72 (Post-Market Monitoring) +- Annex III (High-Risk AI Systems) +- Annex IV (Technical Documentation Requirements) +- Annex VIII (Registration Information) + +### 3. `assess_compliance` + +AI-powered compliance assessment and documentation generator using one of 5 supported models. + +**Supported Models:** +| Model ID | Provider | Model Name | +| ------------- | --------- | --------------------------- | +| `claude-4.5` | Anthropic | Claude Sonnet 4.5 (default) | +| `claude-opus` | Anthropic | Claude Opus 4 | +| `gpt-5` | OpenAI | GPT-5 | +| `grok-4-1` | xAI | Grok 4.1 Fast Reasoning | +| `gemini-3` | Google | Gemini 3 Pro | + +**Input:** +```json +{ + "organizationContext": { /* from discover_organization */ }, + "aiServicesContext": { /* from discover_ai_services */ }, + "focusAreas": ["Technical Documentation", "Risk Management"], + "generateDocumentation": true, + "model": "claude-4.5" // Optional: override AI_MODEL env var +} +``` + +> **New:** The `model` parameter allows direct model selection per request, taking precedence over the `AI_MODEL` environment variable. + +**Output Schema:** +```typescript +{ + assessment: { + overallScore: number; // 0-100 compliance score + riskLevel: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"; + gaps: Array<{ + id: string; + severity: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"; + category: string; + description: string; + affectedSystems: string[]; + articleReference: string; + currentState: string; + requiredState: string; + remediationEffort: "LOW" | "MEDIUM" | "HIGH"; + estimatedCost?: string; + deadline?: string; + }>; + recommendations: Array<{ + id: string; + priority: number; // 1-10 (1 highest) + title: string; + description: string; + articleReference: string; + implementationSteps: string[]; + estimatedEffort: string; + expectedOutcome: string; + dependencies?: string[]; + }>; + complianceByArticle: Record; + }; + documentation?: { + // ✅ CURRENTLY GENERATED (for speed & cost optimization): + riskManagementTemplate?: string; // Article 9 - Markdown template + technicalDocumentation?: string; // Article 11 - Markdown template + + // 🔜 PLANNED (not yet implemented - omitted for API cost & speed): + // conformityAssessment?: string; // Article 43 - Markdown template + // transparencyNotice?: string; // Article 50 - Markdown template + // qualityManagementSystem?: string; // Article 17 - Markdown template + // humanOversightProcedure?: string; // Article 14 - Markdown template + // dataGovernancePolicy?: string; // Article 10 - Markdown template + // incidentReportingProcedure?: string; // Incident reporting template + }; + reasoning: string; // Chain-of-thought explanation + metadata: { + assessmentDate: string; + assessmentVersion: string; + modelUsed: string; // e.g., "claude-4-5", "gpt-5", or "grok-4-1" + organizationAssessed?: string; + systemsAssessed: string[]; + focusAreas: string[]; + documentationFiles?: string[]; // Paths to generated markdown files + }; +} +``` + +> ⚠️ **Note on Documentation Generation:** Currently, only **2 documentation templates** are generated: +> - ⚡ **Risk Management System** (Article 9) +> - 📋 **Technical Documentation** (Article 11 / Annex IV) +> +> The remaining 6 templates (Conformity Assessment, Transparency Notice, Quality Management System, Human Oversight Procedure, Data Governance Policy, Incident Reporting Procedure) are **planned but not yet implemented** to optimize API costs and response speed during the hackathon demo. +``` + +**EU AI Act References:** +- Article 9 (Risk Management System) +- Article 10 (Data Governance) +- Article 11 (Technical Documentation) +- Article 12 (Record-Keeping) +- Article 13 (Transparency) +- Article 14 (Human Oversight) +- Article 15 (Accuracy, Robustness, Cybersecurity) +- Article 17 (Quality Management System) +- Article 43 (Conformity Assessment) +- Article 49 (EU Database Registration) +- Article 50 (Transparency Obligations) +- Annex IV (Technical Documentation Requirements) + +**Requirements:** +- `TAVILY_API_KEY` environment variable must be set (required for web research) +- One of the following model API keys must be set (based on model selection): + - `ANTHROPIC_API_KEY` for Claude 4.5 or Claude Opus + - `OPENAI_API_KEY` for GPT-5 + - `XAI_API_KEY` for Grok 4.1 + - `GOOGLE_GENERATIVE_AI_API_KEY` for Gemini 3 Pro +- Model can be selected via: + - `model` parameter in tool input (takes precedence) + - `AI_MODEL` environment variable + - Defaults to `claude-4.5` if not specified +- Uses the selected model for intelligent compliance analysis +- Generates professional documentation templates in Markdown + +## Testing + +Run the test agent to verify the MCP server: + +```bash +# Run test agent +pnpm --filter @eu-ai-act/test-agent dev +``` + +The test agent will: +1. Connect to the MCP server +2. List available tools +3. Test organization discovery +4. Test AI services discovery +5. Test AI-powered compliance assessment (requires `TAVILY_API_KEY` and one model API key) +6. Display compliance gaps and recommendations +7. Show generated documentation templates + +**Required API keys for testing:** +- `TAVILY_API_KEY` (always required) +- One of: `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY`, or `GOOGLE_GENERATIVE_AI_API_KEY` + +## ChatGPT App Deployment + +To deploy as a ChatGPT App with HTTP/SSE transport: + +1. **Create Express Server:** + +```typescript +import express from 'express'; +import cors from 'cors'; +import { createChatGPTAppServer, handleMCPEndpoint, handleMCPMessages } from '@eu-ai-act/mcp-server/chatgpt-app'; + +const app = express(); +app.use(cors()); +app.use(express.json()); + +const mcpServer = createChatGPTAppServer({ + name: "EU AI Act Compliance", + description: "EU AI Act compliance tools for organization and AI systems discovery", + version: "0.1.0", + baseUrl: "https://your-app.com" +}); + +app.get('/mcp', (req, res) => handleMCPEndpoint(mcpServer, req, res)); +app.post('/mcp/messages', (req, res) => handleMCPMessages(mcpServer, req, res)); + +app.listen(3000, () => { + console.log('MCP Server running on http://localhost:3000'); +}); +``` + +2. **Deploy to Cloud Provider** (Vercel, Railway, Render, etc.) + +3. **Configure in ChatGPT:** + - Go to Settings → Apps & Connectors → Advanced settings + - Enable developer mode + - Add connector with your deployment URL + `/mcp` + - Test the connection + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ MCP Clients │ +│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ +│ │ Claude │ │ ChatGPT │ │ Custom Client │ │ +│ │ Desktop │ │ Apps │ │ (MCP SDK) │ │ +│ └────┬─────┘ └────┬─────┘ └────────┬─────────┘ │ +└────────┼─────────────┼─────────────────┼───────────────┘ + │ │ │ + └─────────────┼──────────────────┘ + ▼ + ┌─────────────────────────────┐ + │ MCP Protocol Layer │ + │ (stdio / HTTP+SSE) │ + └─────────────┬───────────────┘ + ▼ + ┌─────────────────────────────┐ + │ EU AI Act MCP Server │ + │ │ + │ ┌─────────────────────┐ │ + │ │ discover_ │ │ + │ │ organization │ │ + │ └─────────────────────┘ │ + │ │ + │ ┌─────────────────────┐ │ + │ │ discover_ │ │ + │ │ ai_services │ │ + │ └─────────────────────┘ │ + │ │ + │ ┌─────────────────────┐ │ + │ │ assess_ │◄──┼──── AI Model (5 models supported) + │ │ compliance │ │ Claude 4.5/Opus | GPT-5 | Grok 4.1 | Gemini 3 + │ └─────────────────────┘ │ + └─────────────────────────────┘ +``` + +## Development + +### Project Structure + +``` +packages/eu-ai-act-mcp/ +├── src/ +│ ├── index.ts # Main MCP server (stdio) +│ ├── chatgpt-app.ts # ChatGPT Apps SDK compatibility +│ ├── types/ +│ │ └── index.ts # TypeScript interfaces +│ ├── schemas/ +│ │ └── index.ts # Zod validation schemas +│ └── tools/ +│ ├── discover-organization.ts # Tavily-powered org discovery +│ ├── discover-ai-services.ts # AI systems inventory +│ └── assess-compliance.ts # Multi-model AI compliance assessment +├── package.json +├── tsconfig.json +└── README.md +``` + +### Adding New Tools + +1. Define types in `src/types/index.ts` +2. Create Zod schema in `src/schemas/index.ts` +3. Implement tool in `src/tools/your-tool.ts` +4. Register tool in `src/index.ts` + +## EU AI Act Compliance + +This MCP server implements compliance tools based on: + +- **Regulation (EU) 2024/1689** - EU Artificial Intelligence Act +- **Official Text**: [EUR-Lex](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=OJ:L_202401689) + +### Key Implementation Timeline + +| Date | Requirement | Articles | +| ----------- | --------------------------------------- | -------------- | +| Feb 2, 2025 | Prohibited AI practices | Article 5 | +| Aug 2, 2025 | GPAI model obligations | Article 53 | +| Aug 2, 2026 | High-risk systems obligations (limited) | Articles 16-29 | +| Aug 2, 2027 | Full AI Act enforcement | All Articles | + +### Supported Risk Categories + +- **Unacceptable Risk** - Prohibited AI systems (Article 5) +- **High Risk** - Systems in Annex III requiring conformity assessment +- **Limited Risk** - Transparency obligations (Article 50) +- **Minimal Risk** - No specific obligations + + +## Contributing + +Contributions are welcome! This is a hackathon project for the MCP 1st Birthday Hackathon. + +## Resources + +- [EU AI Act Official Text](https://eur-lex.europa.eu/eli/reg/2024/1689/oj) +- [AI Act Explorer](https://artificialintelligenceact.eu/) +- [Model Context Protocol](https://modelcontextprotocol.io/) +- [Vercel AI SDK](https://ai-sdk.dev/) +- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) + +### AI Provider Documentation +- [Anthropic API](https://docs.anthropic.com/) +- [OpenAI API](https://platform.openai.com/docs) +- [xAI API](https://docs.x.ai/) +- [Google AI](https://ai.google.dev/docs) + +--- + +**Built for the MCP 1st Birthday Hackathon** 🎂 + diff --git a/packages/eu-ai-act-mcp/TAVILY_EXAMPLE.md b/packages/eu-ai-act-mcp/TAVILY_EXAMPLE.md new file mode 100644 index 0000000000000000000000000000000000000000..3ded54e9ec36ba776ac27d9e314055737fc738c4 --- /dev/null +++ b/packages/eu-ai-act-mcp/TAVILY_EXAMPLE.md @@ -0,0 +1,267 @@ +# Tavily AI Integration Example + +This document demonstrates how the Tavily AI integration enhances organization discovery for EU AI Act compliance. + +## Setup + +1. **Get your Tavily API Key:** + - Visit [https://app.tavily.com](https://app.tavily.com) + - Sign up (free tier: 1,000 API credits/month) + - Copy your API key + +2. **Configure environment:** + ```bash + export TAVILY_API_KEY=tvly-YOUR_API_KEY + ``` + +## Example 1: Discovering a Real Company + +### Using with Claude Desktop + +```bash +# In Claude Desktop, use the MCP tool: +discover_organization( + organizationName="OpenAI", + domain="openai.com", + context="AI research company" +) +``` + +### Expected Output with Tavily + +```json +{ + "organization": { + "name": "OpenAI", + "sector": "Technology", + "size": "Enterprise", + "jurisdiction": ["United States"], + "euPresence": false, + "headquarters": { + "country": "United States", + "city": "San Francisco" + }, + "contact": { + "email": "contact@openai.com", + "website": "https://openai.com" + }, + "aiMaturityLevel": "Leader", + "aiSystemsCount": 0, + "primaryRole": "Provider" + }, + "regulatoryContext": { + "applicableFrameworks": ["EU AI Act", "GDPR"], + "complianceDeadlines": [...], + "existingCertifications": ["SOC 2", "ISO 27001"], + "hasQualityManagementSystem": true, + "hasRiskManagementSystem": false + }, + "metadata": { + "createdAt": "2024-11-27T...", + "lastUpdated": "2024-11-27T...", + "completenessScore": 90, + "dataSource": "tavily-research", + "tavilyResults": { + "overview": "OpenAI is an AI research and deployment company...", + "aiCapabilities": "OpenAI develops advanced AI models including GPT-4...", + "compliance": "OpenAI maintains SOC 2 Type II certification...", + "sources": [ + "https://openai.com/about", + "https://openai.com/security" + ] + } + } +} +``` + +## Example 2: European Healthcare AI Company + +```bash +# Discover a healthcare AI company +discover_organization( + organizationName="Ada Health", + domain="ada.com", + context="Digital health AI diagnostic company" +) +``` + +### What Tavily Discovers: + +1. **Company Overview Search:** + - Business model: B2B2C health platform + - Products: AI symptom checker, clinical decision support + - Market: European healthcare market + +2. **AI/Technology Search:** + - AI capabilities: Medical AI, NLP for symptoms + - Maturity: Mature AI operations + - Technology stack: Machine learning models + +3. **Compliance Search:** + - GDPR compliance status + - ISO 27001 certification + - Medical device regulations (MDR) + - Quality management systems + +### Expected Classifications: + +- **Risk Category:** High-risk (healthcare AI per Annex III) +- **EU Presence:** Yes (headquartered in Berlin) +- **Applicable Frameworks:** EU AI Act, GDPR, MDR +- **AI Maturity:** Mature + +## Example 3: Without Tavily (Fallback Mode) + +If `TAVILY_API_KEY` is not set, the tool falls back to mock data: + +```json +{ + "organization": { + "name": "Example Company", + "sector": "Technology", + "size": "SME", + "jurisdiction": ["EU"], + "euPresence": true, + ... + }, + "metadata": { + "completenessScore": 40, + "dataSource": "fallback-mock" + } +} +``` + +## Multi-Step Research Process + +The Tavily integration performs three parallel searches: + +```typescript +// 1. Company Overview +tavily.search( + "Company Name overview business model products services", + { searchDepth: "advanced", maxResults: 5 } +) + +// 2. AI Capabilities +tavily.search( + "Company Name artificial intelligence AI machine learning", + { searchDepth: "basic", maxResults: 3 } +) + +// 3. Compliance Status +tavily.search( + "Company Name GDPR compliance certifications ISO EU regulations", + { searchDepth: "basic", maxResults: 3 } +) +``` + +## Key Benefits + +### 🎯 Accurate Classification +- Real business sector identification (Healthcare, Finance, etc.) +- Actual company size (Startup, SME, Enterprise) +- Verified jurisdiction and EU presence + +### 🔍 AI Maturity Assessment +- Detects AI leadership and pioneering companies +- Identifies mature vs. developing AI capabilities +- Discovers AI-powered products and services + +### ✅ Compliance Discovery +- Finds existing certifications (ISO 27001, SOC 2, GDPR) +- Identifies quality management systems +- Discovers regulatory compliance status + +### 📚 Source Citations +- Provides URLs to company information +- Includes AI-generated summaries +- Links to compliance documentation + +## Tavily Search Parameters + +The integration uses these optimized parameters: + +| Parameter | Value | Reason | +|-----------|-------|--------| +| `searchDepth` | `"advanced"` for overview, `"basic"` for others | Balance between quality and API credits | +| `maxResults` | 5 for overview, 3 for others | Sufficient context without overload | +| `includeAnswer` | `true` | Get AI-generated summaries | +| `includeDomains` | Company domain when provided | Focus on official sources | + +## API Credits Usage + +- **Overview Search (advanced):** ~2 credits +- **AI Search (basic):** ~1 credit +- **Compliance Search (basic):** ~1 credit +- **Total per organization:** ~4 credits + +With the free tier (1,000 credits/month), you can discover **~250 organizations per month**. + +## Integration with EU AI Act + +The discovered information directly maps to AI Act requirements: + +| Tavily Data | AI Act Article | Purpose | +|-------------|----------------|---------| +| Company sector | Annex III | Determine if high-risk sector | +| AI capabilities | Article 6 | Risk classification | +| EU presence | Article 22 | Authorized representative need | +| Certifications | Article 17 | Quality management system | +| Company size | Article 16 | SME support measures | + +## Testing the Integration + +```bash +# Build the MCP server +cd packages/eu-ai-act-mcp +pnpm build + +# Set your API key +export TAVILY_API_KEY=tvly-YOUR_API_KEY + +# Run the test agent +cd ../test-agent +pnpm dev +``` + +The test agent will demonstrate the Tavily integration with real company research. + +## Troubleshooting + +### Issue: "TAVILY_API_KEY not set, using fallback mock data" + +**Solution:** Set the environment variable: +```bash +export TAVILY_API_KEY=tvly-YOUR_API_KEY +``` + +### Issue: Low completeness scores + +**Possible causes:** +- Company has limited public information +- Domain name doesn't match company name +- Recent startup with minimal web presence + +**Solutions:** +- Provide more context in the `context` parameter +- Ensure correct domain is provided +- Check if company website is accessible + +### Issue: API rate limiting + +**Solution:** Monitor your Tavily dashboard for credit usage. Consider: +- Upgrading to a paid plan for higher limits +- Caching organization profiles to reduce API calls +- Using `searchDepth: "basic"` for all searches (saves credits) + +## Resources + +- [Tavily Documentation](https://docs.tavily.com/) +- [Tavily JavaScript SDK](https://docs.tavily.com/sdk/javascript/reference) +- [Company Research Use Case](https://docs.tavily.com/examples/use-cases/company-research) +- [EU AI Act Text](https://eur-lex.europa.eu/eli/reg/2024/1689/oj) + +--- + +**Built for MCP 1st Birthday Hackathon** 🎂 + diff --git a/packages/eu-ai-act-mcp/biome.json b/packages/eu-ai-act-mcp/biome.json new file mode 100644 index 0000000000000000000000000000000000000000..db8f245714dad9ac54ffab0fb57ea921e9a650e0 --- /dev/null +++ b/packages/eu-ai-act-mcp/biome.json @@ -0,0 +1,20 @@ +{ + "root": false, + "extends": ["../../biome.json"], + "linter": { + "rules": { + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } + } + } +} diff --git a/packages/eu-ai-act-mcp/package.json b/packages/eu-ai-act-mcp/package.json new file mode 100644 index 0000000000000000000000000000000000000000..2a293e01bd4561eb0ad4f563209df4ab3574a1f7 --- /dev/null +++ b/packages/eu-ai-act-mcp/package.json @@ -0,0 +1,51 @@ +{ + "name": "@eu-ai-act/mcp-server", + "version": "0.1.0", + "description": "MCP Server for EU AI Act Compliance - Organization & AI Systems Discovery", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "bin": { + "eu-ai-act-mcp": "./dist/index.js" + }, + "exports": { + ".": "./dist/index.js", + "./tools/*": "./dist/tools/*.js", + "./types": "./dist/types/index.js", + "./schemas": "./dist/schemas/index.js" + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsup", + "start": "node dist/index.js", + "typecheck": "tsc --noEmit", + "lint": "biome check .", + "lint:fix": "biome check --write ." + }, + "dependencies": { + "@ai-sdk/anthropic": "^2.0.50", + "@ai-sdk/google": "^2.0.44", + "@ai-sdk/openai": "^2.0.74", + "@ai-sdk/xai": "^2.0.39", + "@modelcontextprotocol/sdk": "^1.23.0", + "@tavily/core": "^0.5.13", + "ai": "^5.0.104", + "colorthief": "^2.6.0", + "openai": "^6.9.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/express": "^5.0.5", + "@types/node": "^22.10.2", + "tsup": "^8.5.1", + "tsx": "^4.20.6" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "eu-ai-act", + "compliance", + "ai-governance", + "chatgpt-apps" + ] +} diff --git a/packages/eu-ai-act-mcp/src/chatgpt-app.ts b/packages/eu-ai-act-mcp/src/chatgpt-app.ts new file mode 100644 index 0000000000000000000000000000000000000000..c4abe173e8cd676d2961dc16f7e2df24d998a3a8 --- /dev/null +++ b/packages/eu-ai-act-mcp/src/chatgpt-app.ts @@ -0,0 +1,167 @@ +/** + * ChatGPT Apps SDK Compatibility Layer + * Provides HTTP/SSE transport for ChatGPT Apps integration + * + * Based on OpenAI Apps SDK documentation: + * https://developers.openai.com/apps-sdk/ + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import type { Request, Response } from "express"; + +/** + * ChatGPT App Configuration + */ +export interface ChatGPTAppConfig { + /** App name for ChatGPT */ + name: string; + + /** App description */ + description: string; + + /** App version */ + version: string; + + /** Base URL for the app (for OAuth/webhooks) */ + baseUrl?: string; + + /** OAuth configuration if needed */ + oauth?: { + clientId: string; + clientSecret: string; + authorizationUrl: string; + tokenUrl: string; + scopes: string[]; + }; +} + +/** + * Create ChatGPT App Server + * + * This creates an MCP server that can be used with ChatGPT Apps SDK + * via HTTP/SSE transport instead of stdio. + * + * Usage with Express: + * ```typescript + * import express from 'express'; + * import { createChatGPTAppServer } from './chatgpt-app.js'; + * + * const app = express(); + * const mcpServer = createChatGPTAppServer({ + * name: "EU AI Act Compliance", + * description: "EU AI Act compliance tools", + * version: "0.1.0" + * }); + * + * // MCP endpoint + * app.get('/mcp', (req, res) => handleMCPEndpoint(mcpServer, req, res)); + * app.post('/mcp/messages', (req, res) => handleMCPMessages(mcpServer, req, res)); + * + * app.listen(3000); + * ``` + */ +export function createChatGPTAppServer(config: ChatGPTAppConfig): McpServer { + const server = new McpServer({ + name: config.name, + version: config.version, + }); + + return server; +} + +/** + * Handle MCP endpoint for ChatGPT Apps + * GET /mcp - Returns MCP server capabilities + */ +export async function handleMCPEndpoint( + server: McpServer, + config: ChatGPTAppConfig, + req: Request, + res: Response +): Promise { + try { + // Return MCP server metadata + res.json({ + name: config.name, + version: config.version, + protocol: "mcp", + transports: ["sse"], + }); + } catch (error) { + console.error("Error handling MCP endpoint:", error); + res.status(500).json({ + error: "Internal server error", + message: error instanceof Error ? error.message : String(error), + }); + } +} + +/** + * Handle MCP messages for ChatGPT Apps + * POST /mcp/messages - Handles tool calls via SSE + */ +export async function handleMCPMessages( + server: McpServer, + req: Request, + res: Response +): Promise { + try { + // Set up Streamable HTTP transport + // @ts-expect-error - StreamableHTTPServerTransport signature varies by SDK version + const transport = new StreamableHTTPServerTransport(res); + + // Connect server to transport + await server.connect(transport); + + // Handle client disconnect + req.on("close", () => { + console.log("Client disconnected from MCP"); + }); + } catch (error) { + console.error("Error handling MCP messages:", error); + if (!res.headersSent) { + res.status(500).json({ + error: "Internal server error", + message: error instanceof Error ? error.message : String(error), + }); + } + } +} + +/** + * ChatGPT App Manifest Generator + * Generates the app manifest required by ChatGPT Apps SDK + */ +export function generateChatGPTAppManifest( + config: ChatGPTAppConfig, + mcpEndpoint: string +): object { + return { + name: config.name, + description: config.description, + version: config.version, + mcp: { + endpoint: mcpEndpoint, + transport: "sse", + }, + authentication: config.oauth + ? { + type: "oauth2", + oauth2: { + clientId: config.oauth.clientId, + authorizationUrl: config.oauth.authorizationUrl, + tokenUrl: config.oauth.tokenUrl, + scopes: config.oauth.scopes, + }, + } + : { + type: "none", + }, + privacy: { + policyUrl: config.baseUrl ? `${config.baseUrl}/privacy` : undefined, + termsUrl: config.baseUrl ? `${config.baseUrl}/terms` : undefined, + }, + }; +} + diff --git a/packages/eu-ai-act-mcp/src/index.ts b/packages/eu-ai-act-mcp/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..14ec43e31b1df953797e9653c6df72603d2d0eac --- /dev/null +++ b/packages/eu-ai-act-mcp/src/index.ts @@ -0,0 +1,353 @@ +/** + * EU AI Act MCP Server + * Model Context Protocol server providing EU AI Act compliance tools + * + * Implements: + * - Organization Discovery (Article 16, 49) + * - AI Systems Discovery (Article 6, 11, Annex III) + * + * Compatible with: + * - Claude Desktop + * - ChatGPT Apps SDK + * - Any MCP-compliant client + */ + +import { config } from "dotenv"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; + +// Load .env from workspace root +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootEnvPath = resolve(__dirname, "../../../.env"); +config({ path: rootEnvPath }); + +import { discoverOrganization } from "./tools/discover-organization.js"; +import { discoverAIServices } from "./tools/discover-ai-services.js"; +import { assessCompliance } from "./tools/assess-compliance.js"; +import { + DiscoverOrganizationInputSchema, + DiscoverAIServicesInputSchema, + ComplianceAssessmentInputSchema, +} from "./schemas/index.js"; +import type { ApiKeys } from "./utils/model.js"; + +// Export shared utilities for external use +export { getModel, type ApiKeys } from "./utils/model.js"; + +/** + * Get API keys from environment variables (set by agent from Gradio UI) + * These are set by the agent when creating the MCP client - they come from Gradio UI user input + * NEVER read from process.env in tool functions - always use passed parameters! + */ +function getApiKeysFromEnv(): ApiKeys { + return { + openaiApiKey: process.env.OPENAI_API_KEY, + anthropicApiKey: process.env.ANTHROPIC_API_KEY, + googleApiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY, + xaiApiKey: process.env.XAI_API_KEY, + modalEndpointUrl: process.env.MODAL_ENDPOINT_URL, + }; +} + +/** + * Get model name from environment variable (set by agent from Gradio UI) + * This is set by the agent when creating the MCP client - it comes from Gradio UI user selection + * NEVER read from process.env.AI_MODEL in tool functions - always use passed parameter! + */ +function getModelFromEnv(): string | undefined { + return process.env.AI_MODEL; +} + +/** + * Get Tavily API key from environment variable (set by agent from Gradio UI) + * This is set by the agent when creating the MCP client - it comes from Gradio UI user input + * NEVER read from process.env.TAVILY_API_KEY in tool functions - always use passed parameter! + */ +function getTavilyKeyFromEnv(): string | undefined { + return process.env.TAVILY_API_KEY; +} + +// Export tool functions for direct API use (ChatGPT Apps, REST API, etc.) +export { discoverOrganization, discoverAIServices, assessCompliance }; + +/** + * MCP Server Instance + */ +const server = new McpServer({ + name: "@eu-ai-act/mcp-server", + version: "0.1.0", +}); + +/** + * Register Tools using McpServer API + * Using registerTool (non-deprecated API) + */ + +// Tool 1: Organization Discovery +server.registerTool( + "discover_organization", + { + description: `Discover and profile an organization for EU AI Act compliance. + +This tool researches an organization and creates a comprehensive profile including: +- Basic organization information (name, sector, size, location) +- Contact information (email, phone, website) +- Regulatory context and compliance deadlines +- AI maturity level assessment +- Certifications and compliance status +- Provider role classification per Article 3(3) +- Quality management system status per Article 17 + +The tool automatically discovers the company domain from web research if not provided. + +Based on EU AI Act Articles 16 (Provider Obligations), 22 (Authorized Representatives), and 49 (Registration Requirements).`, + inputSchema: z.object({ + organizationName: z.string().describe("Name of the organization to discover (required)"), + domain: z.string().optional().nullable().describe("Organization's domain (e.g., 'ibm.com'). Auto-discovered if not provided."), + context: z.string().optional().nullable().describe("Additional context about the organization"), + }), + }, + async ({ organizationName, domain, context }: { organizationName: string; domain?: string | null; context?: string | null }) => { + // Convert null values to undefined + const cleanDomain = domain ?? undefined; + const cleanContext = context ?? undefined; + + // Get API keys and model from env vars (set by agent from Gradio UI) + const apiKeys = getApiKeysFromEnv(); + const model = process.env.AI_MODEL; + const tavilyApiKey = process.env.TAVILY_API_KEY; + + console.error(`[discover_organization] Called with: organizationName="${organizationName}", domain="${cleanDomain}", context="${cleanContext}"`); + + try { + // Execute tool with API keys from Gradio UI + const result = await discoverOrganization({ + organizationName, + domain: cleanDomain, + context: cleanContext, + model, + apiKeys, + tavilyApiKey, + }); + + console.error(`[discover_organization] Completed, result has ${JSON.stringify(result).length} chars`); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + console.error(`[discover_organization] Error:`, error); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: true, + message: error instanceof Error ? error.message : "Unknown error occurred", + tool: "discover_organization", + }, null, 2), + }, + ], + isError: true, + }; + } + } +); + +// Tool 2: AI Services Discovery +server.registerTool( + "discover_ai_services", + { + description: `Discover and classify AI systems within an organization per EU AI Act requirements. + +This tool scans for AI systems and provides comprehensive compliance analysis: +- Risk classification per Article 6 and Annex III (Unacceptable, High, Limited, Minimal) +- Technical documentation status per Article 11 +- Conformity assessment requirements per Article 43 +- Compliance gap analysis with specific Article references +- Registration status per Article 49 + +Generates reports for systems requiring immediate attention with EU database registration obligations (Article 49, Annex VIII).`, + inputSchema: z.object({ + organizationContext: z.any().optional().nullable().describe("Organization profile from discover_organization tool (optional but recommended)"), + systemNames: z.array(z.string()).optional().nullable().describe("Specific AI system names to discover (optional, scans all if not provided)"), + scope: z.string().optional().nullable().describe("Scope of discovery: 'all' (default), 'high-risk-only', 'production-only'"), + context: z.string().optional().nullable().describe("Additional context about the systems"), + }), + }, + async ({ organizationContext, systemNames, scope, context }: { organizationContext?: any; systemNames?: string[] | null; scope?: string | null; context?: string | null }) => { + // Convert null values to undefined + const cleanOrgContext = organizationContext ?? undefined; + const cleanSystemNames = systemNames ?? undefined; + const cleanScope = scope ?? undefined; + const cleanContext = context ?? undefined; + + // Get API keys, model, and Tavily key from env vars (set by agent from Gradio UI) + // These come from the Gradio UI user input - NEVER read from process.env in tool functions! + const apiKeys = getApiKeysFromEnv(); + const model = getModelFromEnv(); + const tavilyApiKey = getTavilyKeyFromEnv(); + + console.error(`[discover_ai_services] Called with: systemNames=${JSON.stringify(cleanSystemNames)}, scope="${cleanScope}"`); + + try { + // Execute tool with API keys from Gradio UI + const result = await discoverAIServices({ + organizationContext: cleanOrgContext, + systemNames: cleanSystemNames, + scope: cleanScope, + context: cleanContext, + model, + apiKeys, + tavilyApiKey, + }); + + console.error(`[discover_ai_services] Completed, found ${result.systems?.length || 0} systems`); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + console.error(`[discover_ai_services] Error:`, error); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: true, + message: error instanceof Error ? error.message : "Unknown error occurred", + tool: "discover_ai_services", + }, null, 2), + }, + ], + isError: true, + }; + } + } +); + +// Tool 3: Compliance Assessment +server.registerTool( + "assess_compliance", + { + description: `Assess EU AI Act compliance and generate documentation using AI analysis. + +This tool takes organization and AI services context to produce comprehensive compliance assessment: +- Gap analysis against AI Act requirements (Articles 9-15, 16-22, 43-50) +- Risk-specific compliance checklists +- Draft documentation templates in markdown format +- Remediation recommendations with priorities +- Overall compliance score (0-100) +- Chain-of-thought reasoning explanation + +Supports multiple AI models (set via 'model' parameter or AI_MODEL environment variable): +- claude-4.5: Anthropic Claude Sonnet 4.5 (default) +- claude-opus: Anthropic Claude Opus 4 +- gpt-5: OpenAI GPT-5 +- grok-4-1: xAI Grok 4.1 Fast Reasoning +- gemini-3: Google Gemini 3 Pro + +Generates professional documentation templates for: +- Risk Management System (Article 9) +- Technical Documentation (Article 11, Annex IV) +- Conformity Assessment (Article 43) +- Transparency Notice (Article 50) +- Quality Management System (Article 17) +- Human Oversight Procedure (Article 14) +- Data Governance Policy (Article 10) + +Requires appropriate API key environment variable based on selected model.`, + inputSchema: z.object({ + organizationContext: z.any().optional().nullable().describe("Organization profile from discover_organization tool (optional)"), + aiServicesContext: z.any().optional().nullable().describe("AI services discovery results from discover_ai_services tool (optional)"), + focusAreas: z.array(z.string()).optional().nullable().describe("Specific compliance areas to focus on (optional)"), + generateDocumentation: z.boolean().optional().nullable().describe("Whether to generate documentation templates (default: true)"), + model: z.enum(["claude-4.5", "claude-opus", "gpt-5", "grok-4-1", "gemini-3"]).optional().nullable().describe("AI model to use: claude-4.5 (default), claude-opus, gpt-5, grok-4-1, gemini-3"), + }), + }, + async ({ organizationContext, aiServicesContext, focusAreas, generateDocumentation, model }: { organizationContext?: any; aiServicesContext?: any; focusAreas?: string[] | null; generateDocumentation?: boolean | null; model?: string | null }) => { + // Convert null values to undefined for downstream functions + const cleanFocusAreas = focusAreas ?? undefined; + const cleanGenerateDocumentation = generateDocumentation ?? undefined; + const cleanOrgContext = organizationContext ?? undefined; + const cleanAiServicesContext = aiServicesContext ?? undefined; + // Get API keys, model, and Tavily key from env vars (set by agent from Gradio UI) + // These come from the Gradio UI user input - NEVER read from process.env in tool functions! + const apiKeys = getApiKeysFromEnv(); + const cleanModel = model ?? getModelFromEnv(); + const tavilyApiKey = getTavilyKeyFromEnv(); + + console.error(`[assess_compliance] Called with: model="${cleanModel}", focusAreas=${JSON.stringify(cleanFocusAreas)}, generateDocumentation=${cleanGenerateDocumentation}`); + + try { + // Execute tool with API keys from Gradio UI + const result = await assessCompliance({ + organizationContext: cleanOrgContext, + aiServicesContext: cleanAiServicesContext, + focusAreas: cleanFocusAreas, + generateDocumentation: cleanGenerateDocumentation, + model: cleanModel as any, + apiKeys, + tavilyApiKey, + }); + + console.error(`[assess_compliance] Completed, score: ${result.assessment?.overallScore || 'N/A'}`); + + return { + content: [ + { + type: "text", + text: JSON.stringify(result, null, 2), + }, + ], + }; + } catch (error) { + console.error(`[assess_compliance] Error:`, error); + return { + content: [ + { + type: "text", + text: JSON.stringify({ + error: true, + message: error instanceof Error ? error.message : "Unknown error occurred", + tool: "assess_compliance", + hint: "Make sure XAI_API_KEY environment variable is set", + }, null, 2), + }, + ], + isError: true, + }; + } + } +); + +/** + * Start Server + */ +async function main() { + const transport = new StdioServerTransport(); + await server.connect(transport); + + console.error("EU AI Act MCP Server running on stdio"); + console.error("Available tools: discover_organization, discover_ai_services, assess_compliance"); +} + +main().catch((error) => { + console.error("Fatal error in main():", error); + process.exit(1); +}); + diff --git a/packages/eu-ai-act-mcp/src/schemas/index.ts b/packages/eu-ai-act-mcp/src/schemas/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..c11c4d4273db250dd878793fd68a7df263abb0f1 --- /dev/null +++ b/packages/eu-ai-act-mcp/src/schemas/index.ts @@ -0,0 +1,324 @@ +/** + * Zod Validation Schemas for EU AI Act MCP Server + * Provides runtime validation and type safety + */ + +import { z } from "zod"; + +/** + * Organization Size Schema + */ +export const OrganizationSizeSchema = z.enum([ + "SME", + "Large Enterprise", + "Public Body", + "Micro Enterprise", +]); + +/** + * AI Maturity Level Schema + */ +export const AIMaturityLevelSchema = z.enum([ + "Nascent", + "Developing", + "Advanced", + "Expert", +]); + +/** + * Risk Category Schema + */ +export const RiskCategorySchema = z.enum([ + "Unacceptable", + "High", + "Limited", + "Minimal", +]); + +/** + * Deployment Model Schema + */ +export const DeploymentModelSchema = z.enum([ + "On-premise", + "Cloud", + "Hybrid", + "Edge", + "SaaS", +]); + +/** + * Provider Role Schema + */ +export const ProviderRoleSchema = z.enum([ + "Provider", + "Deployer", + "Importer", + "Distributor", + "Authorized Representative", +]); + +/** + * Conformity Assessment Type Schema + */ +export const ConformityAssessmentTypeSchema = z.enum([ + "Internal Control", + "Third Party Assessment", + "Not Required", + "Pending", +]); + +/** + * Organization Profile Schema + */ +export const OrganizationProfileSchema = z.object({ + organization: z.object({ + name: z.string(), + registrationNumber: z.string().optional(), + sector: z.string(), + size: OrganizationSizeSchema, + jurisdiction: z.array(z.string()), + euPresence: z.boolean(), + headquarters: z.object({ + country: z.string(), + city: z.string(), + address: z.string().optional(), + }), + contact: z.object({ + email: z.string().email(), + phone: z.string().optional(), + website: z.string().url().optional(), + }), + branding: z.object({ + logoUrl: z.string().url().optional(), + primaryColor: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional(), + palette: z.array(z.string().regex(/^#[0-9A-Fa-f]{6}$/)).optional(), + source: z.enum(["logo-extraction", "known-brand", "fallback"]).optional(), + }).optional(), + aiMaturityLevel: AIMaturityLevelSchema, + aiSystemsCount: z.number().optional(), + primaryRole: ProviderRoleSchema, + }), + regulatoryContext: z.object({ + applicableFrameworks: z.array(z.string()), + complianceDeadlines: z.array( + z.object({ + date: z.string(), + description: z.string(), + article: z.string(), + }) + ), + existingCertifications: z.array(z.string()), + hasAuthorizedRepresentative: z.boolean().optional(), + notifiedBodyId: z.string().optional(), + hasQualityManagementSystem: z.boolean(), + hasRiskManagementSystem: z.boolean(), + }), + metadata: z.object({ + createdAt: z.string(), + lastUpdated: z.string(), + completenessScore: z.number().min(0).max(100), + dataSource: z.string(), + }), +}); + +/** + * AI System Profile Schema + */ +export const AISystemProfileSchema = z.object({ + system: z.object({ + name: z.string(), + systemId: z.string().optional(), + description: z.string(), + intendedPurpose: z.string(), + version: z.string(), + status: z.enum(["Development", "Testing", "Production", "Deprecated"]), + provider: z.object({ + name: z.string(), + role: ProviderRoleSchema, + contact: z.string(), + }), + }), + riskClassification: z.object({ + category: RiskCategorySchema, + annexIIICategory: z.string().optional(), + justification: z.string(), + safetyComponent: z.boolean(), + riskScore: z.number().min(0).max(100), + conformityAssessmentRequired: z.boolean(), + conformityAssessmentType: ConformityAssessmentTypeSchema, + }), + technicalDetails: z.object({ + aiTechnology: z.array(z.string()), + dataProcessed: z.array(z.string()), + processesSpecialCategoryData: z.boolean(), + deploymentModel: DeploymentModelSchema, + vendor: z.string().optional(), + trainingData: z + .object({ + description: z.string(), + sources: z.array(z.string()), + biasAssessment: z.boolean(), + }) + .optional(), + integrations: z.array(z.string()), + humanOversight: z.object({ + enabled: z.boolean(), + description: z.string().optional(), + }), + }), + complianceStatus: z.object({ + hasTechnicalDocumentation: z.boolean(), + conformityAssessmentStatus: z.enum([ + "Not Started", + "In Progress", + "Completed", + "Not Required", + ]), + hasEUDeclaration: z.boolean(), + hasCEMarking: z.boolean(), + registeredInEUDatabase: z.boolean(), + hasPostMarketMonitoring: z.boolean(), + hasAutomatedLogging: z.boolean(), + lastAssessmentDate: z.string().optional(), + identifiedGaps: z.array(z.string()), + }), + metadata: z.object({ + createdAt: z.string(), + lastUpdated: z.string(), + dataSource: z.string(), + discoveryMethod: z.string(), + }), +}); + +/** + * Discovery Input Schemas + */ +export const DiscoverOrganizationInputSchema = z.object({ + organizationName: z.string().min(1, "Organization name is required"), + domain: z.string().optional(), + context: z.string().optional(), +}); + +export const DiscoverAIServicesInputSchema = z.object({ + organizationContext: OrganizationProfileSchema.optional(), + systemNames: z.array(z.string()).optional(), + scope: z.string().optional(), +}); + +/** + * AI Systems Discovery Response Schema + */ +export const AISystemsDiscoveryResponseSchema = z.object({ + systems: z.array(AISystemProfileSchema), + riskSummary: z.object({ + unacceptableRiskCount: z.number(), + highRiskCount: z.number(), + limitedRiskCount: z.number(), + minimalRiskCount: z.number(), + totalCount: z.number(), + }), + complianceSummary: z.object({ + fullyCompliantCount: z.number(), + partiallyCompliantCount: z.number(), + nonCompliantCount: z.number(), + requiresAttention: z.array(AISystemProfileSchema), + }), + discoveryMetadata: z.object({ + timestamp: z.string(), + method: z.string(), + coverage: z.string(), + }), +}); + +/** + * Gap Severity Schema + */ +export const GapSeveritySchema = z.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]); + +/** + * Remediation Effort Schema + */ +export const RemediationEffortSchema = z.enum(["LOW", "MEDIUM", "HIGH"]); + +/** + * Gap Analysis Schema + */ +export const GapAnalysisSchema = z.object({ + id: z.string(), + severity: GapSeveritySchema, + category: z.string(), + description: z.string(), + affectedSystems: z.array(z.string()), + articleReference: z.string(), + currentState: z.string(), + requiredState: z.string(), + remediationEffort: RemediationEffortSchema, + estimatedCost: z.string().optional(), + deadline: z.string().optional(), +}); + +/** + * Recommendation Schema + */ +export const RecommendationSchema = z.object({ + id: z.string(), + priority: z.number().min(1).max(10), + title: z.string(), + description: z.string(), + articleReference: z.string(), + implementationSteps: z.array(z.string()), + estimatedEffort: z.string(), + expectedOutcome: z.string(), + dependencies: z.array(z.string()).optional(), +}); + +/** + * Compliance Documentation Schema + */ +export const ComplianceDocumentationSchema = z.object({ + riskManagementTemplate: z.string().optional(), + technicalDocumentation: z.string().optional(), + conformityAssessment: z.string().optional(), + transparencyNotice: z.string().optional(), + qualityManagementSystem: z.string().optional(), + humanOversightProcedure: z.string().optional(), + dataGovernancePolicy: z.string().optional(), + incidentReportingProcedure: z.string().optional(), +}); + +/** + * Compliance Assessment Input Schema + */ +export const ComplianceAssessmentInputSchema = z.object({ + organizationContext: OrganizationProfileSchema.optional(), + aiServicesContext: AISystemsDiscoveryResponseSchema.optional(), + focusAreas: z.array(z.string()).optional(), + generateDocumentation: z.boolean().optional().default(true), +}); + +/** + * Compliance Assessment Response Schema + */ +export const ComplianceAssessmentResponseSchema = z.object({ + assessment: z.object({ + overallScore: z.number().min(0).max(100), + riskLevel: GapSeveritySchema, + gaps: z.array(GapAnalysisSchema), + recommendations: z.array(RecommendationSchema), + complianceByArticle: z.record(z.object({ + compliant: z.boolean(), + gaps: z.array(z.string()), + })), + }), + documentation: ComplianceDocumentationSchema.optional(), + reasoning: z.string(), + metadata: z.object({ + assessmentDate: z.string(), + assessmentVersion: z.string(), + modelUsed: z.string(), + organizationAssessed: z.string().optional(), + systemsAssessed: z.array(z.string()), + focusAreas: z.array(z.string()), + }), +}); + diff --git a/packages/eu-ai-act-mcp/src/tools/assess-compliance.ts b/packages/eu-ai-act-mcp/src/tools/assess-compliance.ts new file mode 100644 index 0000000000000000000000000000000000000000..4af9549faa637df927a6dbcaf3db4f441d74046c --- /dev/null +++ b/packages/eu-ai-act-mcp/src/tools/assess-compliance.ts @@ -0,0 +1,646 @@ +/** + * Compliance Assessment Tool - FAST MODE + * EU AI Act compliance analysis optimized for speed + * Generates brief outputs for agent processing + */ + +import { generateText } from "ai"; +import { writeFile, mkdir } from "fs/promises"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; +import type { + OrganizationProfile, + AISystemsDiscoveryResponse, + ComplianceAssessmentInput, + ComplianceAssessmentResponse, + GapAnalysis, + Recommendation, + ComplianceDocumentation, +} from "../types/index.js"; +import { getModel, type ApiKeys } from "../utils/model.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * EU AI Act guidelines for compliance assessment + * Based on Regulation (EU) 2024/1689 - Official Journal L 2024/1689, 12.7.2024 + */ +const EU_AI_ACT_BRIEF = `You are an expert EU AI Act compliance consultant. You have deep knowledge of: + +**EU AI Act (Regulation (EU) 2024/1689)** + +**Risk Classification (Article 6, Annex III):** +- Unacceptable Risk (Article 5): Prohibited AI practices including social scoring, manipulation, exploitation +- High-Risk (Annex III): Employment, education, credit, law enforcement, critical infrastructure, biometric, LEGAL/JUSTICE +- Limited Risk (Article 50): Transparency obligations for chatbots, emotion recognition, deepfakes +- Minimal Risk: General purpose AI with no specific obligations + +**ANNEX III - HIGH-RISK AI SYSTEMS (Complete List):** +1. Biometrics (remote biometric identification, categorisation) +2. Critical infrastructure (safety components in water, gas, electricity, transport) +3. Education and vocational training (determining access, evaluating learning outcomes) +4. Employment and workers management (recruitment, task allocation, termination decisions) +5. Access to essential services (credit scoring, emergency services, health/life insurance) +6. Law enforcement (risk assessment, polygraphs, evidence reliability, profiling) +7. Migration, asylum, border control (polygraphs, risk assessment, verification of documents) +8. **Administration of justice and democratic processes:** + - **Point 8(a): AI systems intended to assist a judicial authority in researching and interpreting facts and the law and in applying the law to a concrete set of facts, or to be used in a similar way in alternative dispute resolution** + - Point 8(b): AI systems intended to influence the outcome of an election or referendum + +**CRITICAL: Legal AI Systems Classification (Annex III, Point 8(a)):** +ANY AI system that provides: +- Legal advice or legal consulting +- Legal research assistance +- Legal document analysis or generation +- Contract review or analysis +- Legal case analysis +- Interpretation of laws or regulations +- Legal risk assessment +- Alternative dispute resolution assistance +- Compliance advice or guidance +Must be classified as HIGH RISK per Annex III, Point 8(a). + +**High-Risk System Requirements (Articles 8-15):** +1. Article 9: Risk Management System - continuous process for identifying, analyzing, mitigating risks +2. Article 10: Data Governance - quality, representativeness, bias detection in training data +3. Article 11: Technical Documentation (Annex IV) - comprehensive system documentation +4. Article 12: Record-Keeping - automatic logging of system operation +5. Article 13: Transparency - clear information to users and deployers +6. Article 14: Human Oversight - appropriate human intervention mechanisms +7. Article 15: Accuracy, Robustness, Cybersecurity - performance and security requirements + +**Provider Obligations (Articles 16-22):** +- Article 16: Provider obligations for high-risk AI +- Article 17: Quality Management System +- Article 22: Authorized Representative (for non-EU providers) + +**Conformity Assessment (Articles 43-49):** +- Article 43: Conformity Assessment Procedures +- Article 47: EU Declaration of Conformity +- Article 48: CE Marking +- Article 49: EU Database Registration + +**Key Deadlines:** +- February 2, 2025: Prohibited AI practices ban +- August 2, 2025: GPAI model obligations +- August 2, 2026: Full enforcement for high-risk systems + +**When analyzing compliance, always:** +1. Reference specific articles and annexes +2. Provide actionable remediation steps +3. Prioritize gaps by severity (CRITICAL, HIGH, MEDIUM, LOW) +4. Calculate realistic compliance scores +5. ALWAYS classify legal/judicial AI systems as HIGH RISK per Annex III Point 8(a)`; + +/** + * HIGH-RISK KEYWORDS from EU AI Act Annex III + */ +const HIGH_RISK_KEYWORDS = [ + "legal", + "law", + "lawyer", + "attorney", + "judicial", + "justice", + "court", + "litigation", + "contract", + "compliance", + "regulatory", + "dispute resolution", + "arbitration", + "recruitment", + "hiring", + "hr", + "employee", + "resume", + "candidate", + "termination", + "credit", + "scoring", + "loan", + "insurance", + "creditworthiness", + "biometric", + "facial recognition", + "fingerprint", + "identity verification", + "education", + "student", + "exam", + "grading", + "admission", + "law enforcement", + "police", + "crime", + "profiling", + "critical infrastructure", + "healthcare", + "medical", + "diagnosis", + "patient", +]; + +/** + * Generate BRIEF assessment prompt + */ +function generateAssessmentPrompt( + organizationContext?: OrganizationProfile, + aiServicesContext?: AISystemsDiscoveryResponse, + focusAreas?: string[], +): string { + let prompt = `${EU_AI_ACT_BRIEF}\n\nASSESS COMPLIANCE:\n`; + + if (organizationContext?.organization) { + const org = organizationContext.organization; + prompt += `Org: ${org.name} | ${org.sector} | ${org.size} | EU:${org.euPresence}\n`; + } + + if (aiServicesContext?.systems) { + const rs = aiServicesContext.riskSummary; + prompt += `Systems: ${rs.totalCount} (High:${rs.highRiskCount}, Ltd:${rs.limitedRiskCount}, Min:${rs.minimalRiskCount})\n`; + for (const s of aiServicesContext.systems.slice(0, 5)) { + prompt += `- ${s.system.name}: ${s.riskClassification.category} | ${s.system.intendedPurpose.slice(0, 50)}\n`; + } + } + + if (focusAreas?.length) prompt += `Focus: ${focusAreas.join(", ")}\n`; + + prompt += `\nJSON OUTPUT (be brief, max 3 gaps, max 3 recommendations): +{"overallScore":<0-100>,"riskLevel":"","gaps":[{"id":"g1","severity":"","category":"","description":"","affectedSystems":[""],"articleReference":"","currentState":"","requiredState":"","remediationEffort":"","deadline":""}],"recommendations":[{"id":"r1","priority":<1-10>,"title":"","description":"<brief>","articleReference":"<Art>","implementationSteps":["<s1>","<s2>"],"estimatedEffort":"<est>","expectedOutcome":"<out>","dependencies":[]}],"reasoning":"<1 sentence>"}`; + + return prompt; +} + +/** + * Validate risk classification + */ +function validateRiskClassification( + rc: any, + ctx?: { name?: string; description?: string; intendedPurpose?: string }, +): any { + if (!rc) return rc; + const result = { ...rc }; + const text = [ + ctx?.name, + ctx?.description, + ctx?.intendedPurpose, + rc.annexIIICategory, + rc.justification, + ] + .join(" ") + .toLowerCase(); + + const hasHighRisk = HIGH_RISK_KEYWORDS.some((k) => text.includes(k)); + const isLegal = [ + "legal", + "law", + "lawyer", + "judicial", + "justice", + "court", + "contract", + "compliance", + ].some((k) => text.includes(k)); + + if ( + isLegal || + hasHighRisk || + rc.riskScore >= 70 || + rc.conformityAssessmentRequired + ) { + result.category = "High"; + if (isLegal) { + result.annexIIICategory = "Annex III, Point 8(a) - Legal/Justice"; + result.riskScore = Math.max(result.riskScore || 0, 85); + } + result.conformityAssessmentRequired = true; + result.conformityAssessmentType = + result.conformityAssessmentType || "Internal Control"; + } + + return result; +} + +/** + * Validate AI services context + */ +function validateAIServicesContext(context: any): any { + if (!context?.systems) return context; + + const systems = context.systems.map((s: any) => ({ + ...s, + riskClassification: validateRiskClassification(s.riskClassification, { + name: s.system?.name, + description: s.system?.description, + intendedPurpose: s.system?.intendedPurpose, + }), + })); + + return { + ...context, + systems, + riskSummary: { + unacceptableRiskCount: systems.filter( + (s: any) => s.riskClassification?.category === "Unacceptable", + ).length, + highRiskCount: systems.filter( + (s: any) => s.riskClassification?.category === "High", + ).length, + limitedRiskCount: systems.filter( + (s: any) => s.riskClassification?.category === "Limited", + ).length, + minimalRiskCount: systems.filter( + (s: any) => s.riskClassification?.category === "Minimal", + ).length, + totalCount: systems.length, + }, + }; +} + +/** + * Parse JSON safely + */ +function parseJSON<T>(content: string): T | null { + try { + const match = content.match(/```(?:json)?\s*([\s\S]*?)```/); + const json = match + ? match[1].trim() + : content.replace(/^[^{]*/, "").replace(/[^}]*$/, ""); + return JSON.parse(json) as T; + } catch { + return null; + } +} + +/** + * Normalize organization context + */ +function normalizeOrgContext(ctx: any): OrganizationProfile { + if (ctx?.organization) return ctx; + return { + organization: { + name: ctx?.name || "Unknown", + sector: ctx?.sector || "Technology", + size: ctx?.size || "Enterprise", + jurisdiction: ctx?.jurisdiction || ["EU"], + euPresence: ctx?.euPresence ?? true, + headquarters: ctx?.headquarters || { + country: "Unknown", + city: "Unknown", + }, + contact: ctx?.contact || { email: "unknown@example.com" }, + aiMaturityLevel: ctx?.aiMaturityLevel || "Developing", + aiSystemsCount: ctx?.aiSystemsCount || 0, + primaryRole: ctx?.primaryRole || "Provider", + }, + regulatoryContext: { + applicableFrameworks: ["EU AI Act", "GDPR"], + complianceDeadlines: [], + existingCertifications: [], + hasQualityManagementSystem: false, + hasRiskManagementSystem: false, + }, + metadata: { + createdAt: new Date().toISOString(), + lastUpdated: new Date().toISOString(), + completenessScore: 50, + dataSource: "input", + }, + }; +} + +/** + * Normalize AI services context + */ +function normalizeServicesContext(ctx: any): AISystemsDiscoveryResponse { + if (ctx?.riskSummary) return ctx; + const systems = ctx?.systems || []; + return { + systems: systems.map((s: any) => ({ + system: { + name: s.name || "Unknown", + systemId: s.systemId || `sys-${Date.now()}`, + description: s.description || "", + intendedPurpose: s.intendedPurpose || "", + version: "1.0", + status: "Production", + provider: { name: "Unknown", role: "Provider", contact: "" }, + }, + riskClassification: { + category: s.riskLevel || "Minimal", + safetyComponent: false, + annexIIICategory: "N/A", + justification: "", + riskScore: 50, + conformityAssessmentRequired: false, + conformityAssessmentType: "Not Required", + regulatoryReferences: [], + }, + technicalDetails: { + aiTechnology: s.aiTechnology || ["ML"], + dataProcessed: [], + processesSpecialCategoryData: false, + deploymentModel: "Cloud", + vendor: "", + integrations: [], + humanOversight: { enabled: true, description: "" }, + }, + complianceStatus: { + hasTechnicalDocumentation: false, + conformityAssessmentStatus: "Not Started", + hasEUDeclaration: false, + hasCEMarking: false, + registeredInEUDatabase: false, + hasPostMarketMonitoring: false, + hasAutomatedLogging: false, + qualityManagementSystem: false, + riskManagementSystem: false, + identifiedGaps: [], + complianceDeadline: "2026-08-02", + estimatedComplianceEffort: "TBD", + }, + metadata: { + createdAt: new Date().toISOString(), + lastUpdated: new Date().toISOString(), + dataSource: "input", + discoveryMethod: "manual", + researchSources: [], + }, + })), + riskSummary: { + unacceptableRiskCount: 0, + highRiskCount: 0, + limitedRiskCount: 0, + minimalRiskCount: systems.length, + totalCount: systems.length, + }, + complianceSummary: { + fullyCompliantCount: 0, + partiallyCompliantCount: systems.length, + nonCompliantCount: 0, + requiresAttention: [], + criticalGapCount: 0, + highGapCount: 0, + overallCompliancePercentage: 50, + }, + regulatoryFramework: { + legislation: "EU AI Act 2024/1689", + officialJournal: "OJ L 2024/1689", + entryIntoForce: "Aug 2024", + implementationTimeline: "Aug 2026", + jurisdiction: "EU", + }, + complianceDeadlines: { + highRisk: "Aug 2026", + limitedRisk: "Jun 2026", + generalGPAI: "Aug 2026", + }, + discoverySources: ["input"], + discoveryMetadata: { + timestamp: new Date().toISOString(), + method: "input", + coverage: `${systems.length} systems`, + researchIntegration: "N/A", + conformityAssessmentUrgency: "Review", + }, + }; +} + +/** + * Generate BRIEF documentation templates with prefilled data + */ +function generateDocTemplates( + orgName: string, + systems: string[], + gaps: GapAnalysis[], +): ComplianceDocumentation { + const sysStr = systems.slice(0, 5).join(", "); + const gapStr = gaps + .slice(0, 3) + .map((g) => `- ${g.category}: ${g.description}`) + .join("\n"); + + return { + riskManagementTemplate: `# Risk Management System - ${orgName} +## Article 9 Compliance + +**Organization:** ${orgName} +**Systems:** ${sysStr} +**Date:** ${new Date().toLocaleDateString()} + +### 1. Risk Identification +[List risks for each AI system] + +### 2. Risk Analysis +[Analyze likelihood and impact] + +### 3. Risk Mitigation +[Document mitigation measures] + +### 4. Monitoring +[Describe ongoing monitoring process] + +### Key Gaps to Address: +${gapStr || "None identified"} +`, + technicalDocumentation: `# Technical Documentation - ${orgName} +## Article 11 / Annex IV Compliance + +**Organization:** ${orgName} +**Systems:** ${sysStr} +**Date:** ${new Date().toLocaleDateString()} + +### 1. System Description +[Describe each AI system's purpose and functionality] + +### 2. Intended Purpose +[Document intended use cases and limitations] + +### 3. Data Governance +[Describe training data, quality measures, bias detection] + +### 4. Performance Metrics +[Document accuracy, robustness, cybersecurity measures] + +### 5. Human Oversight +[Describe human intervention mechanisms] + +### Key Gaps to Address: +${gapStr || "None identified"} +`, + }; +} + +/** + * Save documentation files + */ +async function saveDocFiles( + docs: ComplianceDocumentation, + assessment: any, + orgName?: string, +): Promise<string[]> { + const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5); + const org = orgName?.replace(/[^a-zA-Z0-9]/g, "_") || "Org"; + const dir = join(process.cwd(), "compliance-docs", `${org}_${ts}`); + + await mkdir(dir, { recursive: true }); + const paths: string[] = []; + + // Save report + const report = `# EU AI Act Compliance Report - ${orgName || "Organization"} +**Date:** ${new Date().toLocaleDateString()} +**Score:** ${assessment.overallScore}/100 | **Risk:** ${assessment.riskLevel} + +## Gaps (${assessment.gaps.length}) +${assessment.gaps.map((g: GapAnalysis) => `- **${g.severity}** ${g.category}: ${g.description} (${g.articleReference})`).join("\n")} + +## Recommendations (${assessment.recommendations.length}) +${assessment.recommendations.map((r: Recommendation) => `- **P${r.priority}** ${r.title}: ${r.description}`).join("\n")} + +## Next Steps +1. Address critical gaps first +2. Complete technical documentation (Art 11) +3. Conduct conformity assessment (Art 43) +4. Register in EU database (Art 49) +`; + const reportPath = join(dir, "00_Compliance_Report.md"); + await writeFile(reportPath, report); + paths.push(reportPath); + + if (docs.riskManagementTemplate) { + const p = join(dir, "01_Risk_Management.md"); + await writeFile(p, docs.riskManagementTemplate); + paths.push(p); + } + if (docs.technicalDocumentation) { + const p = join(dir, "02_Technical_Docs.md"); + await writeFile(p, docs.technicalDocumentation); + paths.push(p); + } + + return paths; +} + +/** + * Main compliance assessment - FAST MODE + */ +export async function assessCompliance( + input: ComplianceAssessmentInput & { + model?: string; + apiKeys?: ApiKeys; + tavilyApiKey?: string; + }, +): Promise<ComplianceAssessmentResponse> { + let { + organizationContext, + aiServicesContext, + focusAreas, + generateDocumentation = true, + model: modelParam, + apiKeys, + } = input; + + if (!modelParam) throw new Error("Model selection required"); + if (!apiKeys) throw new Error("API keys required"); + + // Normalize inputs + if (organizationContext && !organizationContext.organization) { + organizationContext = normalizeOrgContext(organizationContext); + } + if (aiServicesContext && !aiServicesContext.riskSummary) { + aiServicesContext = normalizeServicesContext(aiServicesContext); + } + if (aiServicesContext) { + aiServicesContext = validateAIServicesContext(aiServicesContext); + } + + console.error( + `\n🔍 Fast Compliance Assessment: ${organizationContext?.organization?.name || "Unknown"}`, + ); + + const model = getModel(modelParam, apiKeys, "assess_compliance"); + + // Single fast call - no streaming, no reasoning + const result = await generateText({ + model, + system: + "EU AI Act compliance expert. Output valid JSON only. Be very brief.", + prompt: generateAssessmentPrompt( + organizationContext, + aiServicesContext, + focusAreas, + ), + temperature: 0, + }); + + const data = parseJSON<{ + overallScore: number; + riskLevel: string; + gaps: GapAnalysis[]; + recommendations: Recommendation[]; + reasoning: string; + }>(result.text); + + if (!data) throw new Error("Failed to parse assessment"); + + console.error( + `✅ Score: ${data.overallScore}/100 | Risk: ${data.riskLevel} | Gaps: ${data.gaps.length}`, + ); + + // Generate brief documentation templates + let documentation: ComplianceDocumentation | undefined; + let docPaths: string[] = []; + + if (generateDocumentation) { + documentation = generateDocTemplates( + organizationContext?.organization?.name || "Organization", + aiServicesContext?.systems?.map((s) => s.system.name) || [], + data.gaps, + ); + + try { + docPaths = await saveDocFiles( + documentation, + data, + organizationContext?.organization?.name, + ); + console.error(`📄 Saved ${docPaths.length} docs`); + } catch (e) { + console.error("⚠️ Doc save failed:", e); + } + } + + const modelMap: Record<string, string> = { + "gpt-5": "openai-gpt-5", + "claude-4.5": "anthropic-claude-sonnet-4-5", + "claude-opus": "anthropic-claude-opus-4", + "gemini-3": "google-gemini-3-pro", + "gpt-oss": "openai-gpt-oss-20b", + "grok-4-1": "xai-grok-4-1", + }; + const modelUsed = modelMap[modelParam] || "unknown"; + + return { + assessment: { + overallScore: data.overallScore, + riskLevel: data.riskLevel as "CRITICAL" | "HIGH" | "MEDIUM" | "LOW", + gaps: data.gaps, + recommendations: data.recommendations, + complianceByArticle: {}, + }, + documentation, + reasoning: data.reasoning, + metadata: { + assessmentDate: new Date().toISOString(), + assessmentVersion: "2.0-fast", + modelUsed, + organizationAssessed: organizationContext?.organization?.name, + systemsAssessed: + aiServicesContext?.systems?.map((s) => s.system.name) || [], + focusAreas: focusAreas || [], + documentationFiles: docPaths.length > 0 ? docPaths : undefined, + }, + }; +} diff --git a/packages/eu-ai-act-mcp/src/tools/discover-ai-services.ts b/packages/eu-ai-act-mcp/src/tools/discover-ai-services.ts new file mode 100644 index 0000000000000000000000000000000000000000..01e1e19d71e12b92609b06f252d632071774aea3 --- /dev/null +++ b/packages/eu-ai-act-mcp/src/tools/discover-ai-services.ts @@ -0,0 +1,1561 @@ +/** + * AI Services Discovery Tool + * Implements EU AI Act Article 6 (Classification), Article 11 (Technical Documentation), + * and Annex III (High-Risk AI Systems) requirements + * + * Research Integration: + * - EU AI Act Regulation (EU) 2024/1689, Official Journal L 2024/1689, 12.7.2024 + * - Annex III: High-Risk AI Systems Categories + * - Article 11: Technical Documentation Requirements + * - Article 43: Conformity Assessment Procedures + * - Article 49: EU Database Registration + * - Article 50: Transparency Obligations + * - Article 72: Post-Market Monitoring Requirements + * - Article 17: Quality Management System + * + * Falls back to AI model when Tavily is not available + */ + +import { tavily } from "@tavily/core"; +import { generateText } from "ai"; +import type { + AISystemProfile, + AISystemsDiscoveryResponse, + DiscoverAIServicesInput, + OrganizationProfile, + RiskCategory, +} from "../types/index.js"; +import { getModel, type ApiKeys } from "../utils/model.js"; + +/** + * Parse JSON response safely from AI model output + */ +function parseJSONResponse<T>(content: string): T | null { + try { + // Try to extract JSON from markdown code blocks if present + const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/); + const jsonStr = jsonMatch ? jsonMatch[1].trim() : content.trim(); + return JSON.parse(jsonStr) as T; + } catch (error) { + console.error("Failed to parse JSON response:", error); + // Try to parse without code blocks + try { + // Remove any leading/trailing non-JSON content + const cleanedContent = content + .replace(/^[^{[]*/, "") + .replace(/[}\]]*[^}\]]*$/, (match) => { + // Keep valid JSON closing brackets + return match.match(/^[}\]]*/)?.[0] || ""; + }); + return JSON.parse(cleanedContent) as T; + } catch { + console.error("Secondary parse also failed"); + return null; + } + } +} + +/** + * Extended AI service discovery with research-backed data + * In production, this would integrate with: + * - Infrastructure scanning (cloud APIs, Kubernetes, etc.) + * - Code repository analysis + * - API gateway logs + * - Service mesh observability + * - ML model registries + * - Tavily API for continuous compliance monitoring + * + * Falls back to AI model when Tavily is not available + */ +async function scanForAISystems( + orgContext?: OrganizationProfile, + systemNames?: string[], + tavilyApiKey?: string, + modelName?: string, + apiKeys?: ApiKeys, +): Promise<AISystemProfile[]> { + // Use passed Tavily API key if provided, otherwise fallback to env var + const apiKey = tavilyApiKey || process.env.TAVILY_API_KEY; + const apiKeySource = tavilyApiKey + ? "user-provided" + : process.env.TAVILY_API_KEY + ? "TAVILY_API_KEY env var" + : "none"; + const organizationName = orgContext?.organization?.name || ""; + + if (!apiKey) { + console.warn( + "⚠️ Tavily API key not provided (neither user key nor TAVILY_API_KEY env var), using AI model for AI systems discovery", + ); + // Use AI model fallback - requires model and apiKeys from Gradio settings + return discoverAISystemsWithAI(orgContext, systemNames, modelName, apiKeys); + } + + console.error(`🔑 Using Tavily API key from: ${apiKeySource}`); + + try { + console.error("\n🔍 Starting Tavily AI Systems Discovery:"); + console.error(`Organization: ${organizationName}`); + + // Log if user specified particular systems to discover + if (systemNames && systemNames.length > 0) { + console.error(`🎯 User-specified systems: ${systemNames.join(", ")}`); + console.error(` Focusing search on these specific systems...`); + } else { + console.error(`🔎 Discovering all AI systems for organization...`); + } + + const client = tavily({ apiKey }); + const now = new Date().toISOString(); + + // Construct specific search queries for this organization + const searchQuery = + systemNames && systemNames.length > 0 + ? `${organizationName} ${systemNames.join(" ")} AI systems artificial intelligence products services machine learning tools applications` + : `${organizationName} AI systems artificial intelligence products services machine learning tools applications chatbot automation recruitment healthcare finance biometric`; + + console.error(`Query: ${searchQuery.substring(0, 100)}...`); + + // Perform comprehensive AI systems discovery search + const searchResults = await client.search(searchQuery, { + searchDepth: "advanced", + maxResults: 10, + includeAnswer: true, + }); + + console.error("\n✅ Tavily Search Complete"); + console.error( + `Answer length: ${searchResults.answer?.length || 0} characters`, + ); + console.error(`Results found: ${searchResults.results?.length || 0}`); + console.error( + `Sources: ${ + searchResults.results + ?.slice(0, 3) + .map((r: any) => r.url) + .join(", ") || "None" + }`, + ); + + // Extract AI systems from search results + const discoveredSystems = await extractAISystemsFromResults( + searchResults, + organizationName, + orgContext, + now, + systemNames, + ); + + if (discoveredSystems.length === 0) { + console.warn("⚠️ No AI systems found in Tavily results"); + // Return empty systems - user should provide Tavily key from Gradio UI + return []; + } + + console.error( + `\n✅ Discovered ${discoveredSystems.length} AI systems for ${organizationName}`, + ); + return discoveredSystems; + } catch (error) { + console.error("❌ Tavily research error:", error); + console.warn("⚠️ Falling back to AI model for AI systems discovery"); + return discoverAISystemsWithAI(orgContext, systemNames, modelName, apiKeys); + } +} + +/** + * Extract AI systems from Tavily search results + */ +async function extractAISystemsFromResults( + searchResults: any, + organizationName: string, + orgContext: OrganizationProfile | undefined, + timestamp: string, + userSpecifiedSystems?: string[], +): Promise<AISystemProfile[]> { + const answer = searchResults.answer || ""; + const results = searchResults.results || []; + const allContent = + answer.toLowerCase() + + results + .map((r: any) => r.content) + .join(" ") + .toLowerCase(); + + console.error("\n📊 AI Systems Extraction Log:"); + console.error("=".repeat(60)); + + if (userSpecifiedSystems && userSpecifiedSystems.length > 0) { + console.error( + `🎯 Filtering for user-specified systems: ${userSpecifiedSystems.join(", ")}`, + ); + } + + const systems: AISystemProfile[] = []; + + // Extract different types of AI systems based on common patterns + // Based on EU AI Act Annex III - High-Risk AI Systems Categories + // Source: https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=OJ:L_202401689 + const systemPatterns = [ + // ANNEX III POINT 8(a) - LEGAL/JUDICIAL AI (HIGH RISK) + { + keywords: [ + "legal", + "law", + "lawyer", + "attorney", + "judicial", + "court", + "litigation", + "contract", + "compliance", + "regulatory", + ], + systemType: "Legal AI Assistant", + systemId: "legal-ai-001", + riskCategory: "High" as RiskCategory, + description: + "AI system for legal assistance, advice, research, or document analysis", + intendedPurpose: + "Legal consulting, contract review, legal research, compliance advice, or interpretation of laws and regulations", + }, + // ANNEX III POINT 4 - EMPLOYMENT (HIGH RISK) + { + keywords: ["recruitment", "hiring", "resume", "candidate", "hr", "job"], + systemType: "Recruitment AI Assistant", + systemId: "rec-ai-001", + riskCategory: "High" as RiskCategory, + description: "AI system for resume screening and candidate ranking", + intendedPurpose: + "Automated screening of job applications and ranking of candidates based on qualifications", + }, + // ANNEX III POINT 6 - LAW ENFORCEMENT (HIGH RISK) + { + keywords: [ + "fraud", + "detection", + "security", + "anomaly", + "law enforcement", + "crime", + "police", + ], + systemType: "Fraud Detection System", + systemId: "fraud-001", + riskCategory: "High" as RiskCategory, + description: "AI system for fraud detection and prevention", + intendedPurpose: + "Automated detection of fraudulent transactions and security threats", + }, + // ANNEX III POINT 5(b) - HEALTHCARE (HIGH RISK) + { + keywords: [ + "healthcare", + "medical", + "diagnosis", + "patient", + "clinical", + "health", + ], + systemType: "Healthcare AI System", + systemId: "health-001", + riskCategory: "High" as RiskCategory, + description: "AI system for healthcare and medical applications", + intendedPurpose: + "Medical diagnosis support, patient data analysis, or clinical decision support", + }, + // ANNEX III POINT 5(b) - CREDIT SCORING (HIGH RISK) + { + keywords: [ + "credit", + "scoring", + "loan", + "financial", + "risk assessment", + "creditworthiness", + ], + systemType: "Credit Scoring System", + systemId: "credit-001", + riskCategory: "High" as RiskCategory, + description: "AI system for credit scoring and financial risk assessment", + intendedPurpose: + "Automated credit evaluation and loan approval recommendations", + }, + // ANNEX III POINT 1 - BIOMETRICS (HIGH RISK) + { + keywords: [ + "biometric", + "facial recognition", + "face", + "fingerprint", + "identity", + ], + systemType: "Biometric Identification System", + systemId: "biometric-001", + riskCategory: "High" as RiskCategory, + description: "AI system for biometric identification and verification", + intendedPurpose: + "Biometric authentication, facial recognition, or identity verification", + }, + // ANNEX III POINT 3 - EDUCATION (HIGH RISK) + { + keywords: [ + "education", + "student", + "academic", + "exam", + "grading", + "admission", + "learning", + ], + systemType: "Education AI System", + systemId: "edu-001", + riskCategory: "High" as RiskCategory, + description: "AI system for educational assessment or admissions", + intendedPurpose: + "Student evaluation, grading, admissions decisions, or learning outcome assessment", + }, + // LIMITED RISK - Chatbots (Article 50) + { + keywords: [ + "chatbot", + "customer support", + "conversational", + "chat", + "virtual assistant", + ], + systemType: "Customer Support Chatbot", + systemId: "cs-bot-001", + riskCategory: "Limited" as RiskCategory, + description: "AI-powered chatbot for customer inquiries and support", + intendedPurpose: + "Automated customer support, FAQ responses, and order status inquiries", + }, + // MINIMAL RISK + { + keywords: [ + "recommendation", + "personalization", + "content", + "product recommendation", + ], + systemType: "Recommendation Engine", + systemId: "rec-engine-001", + riskCategory: "Minimal" as RiskCategory, + description: "AI-powered recommendation and personalization system", + intendedPurpose: + "Product or content recommendations based on user behavior and preferences", + }, + { + keywords: ["translation", "language", "nlp", "text processing"], + systemType: "Language Processing System", + systemId: "nlp-001", + riskCategory: "Minimal" as RiskCategory, + description: "AI system for natural language processing and translation", + intendedPurpose: "Text analysis, translation, or language understanding", + }, + ]; + + // Check which AI systems are mentioned for this organization + for (const pattern of systemPatterns) { + const hasKeywords = pattern.keywords.some((keyword) => + allContent.includes(keyword), + ); + + // If user specified systems, check if this pattern matches any of them + let matchesUserRequest = true; + if (userSpecifiedSystems && userSpecifiedSystems.length > 0) { + matchesUserRequest = userSpecifiedSystems.some((userSystem) => { + const userSystemLower = userSystem.toLowerCase(); + // Check if the user-specified name matches the system type or keywords + return ( + pattern.systemType.toLowerCase().includes(userSystemLower) || + userSystemLower.includes( + pattern.systemType.toLowerCase().split(" ")[0], + ) || + pattern.keywords.some( + (keyword) => + userSystemLower.includes(keyword) || + keyword.includes(userSystemLower), + ) + ); + }); + } + + if (hasKeywords && matchesUserRequest) { + console.error(`✅ Found: ${pattern.systemType}`); + if (userSpecifiedSystems && userSpecifiedSystems.length > 0) { + console.error(` ✓ Matches user-specified system requirement`); + } + + // Create AI system profile + const system: AISystemProfile = createAISystemProfile( + pattern, + organizationName, + orgContext, + timestamp, + searchResults, + ); + + systems.push(system); + } else if (hasKeywords && !matchesUserRequest) { + console.error( + `⊘ Found ${pattern.systemType} but doesn't match user-specified systems, skipping...`, + ); + } + } + + // If user specified systems but we didn't find any matches, try to extract from content directly + if ( + userSpecifiedSystems && + userSpecifiedSystems.length > 0 && + systems.length === 0 + ) { + console.error("\n⚠️ No pattern matches found for user-specified systems."); + console.error(" Attempting direct extraction from search results..."); + + for (const userSystem of userSpecifiedSystems) { + const userSystemLower = userSystem.toLowerCase(); + + // Check if this system is mentioned in the content + if ( + allContent.includes(userSystemLower) || + answer.toLowerCase().includes(userSystemLower) + ) { + console.error(`✅ Found mention of: ${userSystem}`); + + // Create a custom system profile based on what we found + const customSystem = createCustomSystemProfile( + userSystem, + organizationName, + orgContext, + timestamp, + searchResults, + allContent, + ); + + systems.push(customSystem); + } + } + } + + console.error("=".repeat(60)); + console.error(`📊 Total systems discovered: ${systems.length}`); + + if (userSpecifiedSystems && userSpecifiedSystems.length > 0) { + const foundSystems = systems.map((s) => s.system.name); + console.error(`🎯 User requested: ${userSpecifiedSystems.join(", ")}`); + console.error(`✅ Found: ${foundSystems.join(", ") || "None"}`); + } + + console.error(""); + + return systems; +} + +/** + * Create custom AI system profile for user-specified system + */ +function createCustomSystemProfile( + systemName: string, + organizationName: string, + orgContext: OrganizationProfile | undefined, + timestamp: string, + searchResults: any, + content: string, +): AISystemProfile { + const sources = + searchResults.results?.slice(0, 5).map((r: any) => r.url) || []; + + // Try to infer risk category from system name and content + let riskCategory: RiskCategory = "Minimal"; + let systemType = systemName; + + // Detect high-risk indicators + if ( + content.includes("recruitment") || + content.includes("hiring") || + systemName.toLowerCase().includes("recruit") + ) { + riskCategory = "High"; + systemType = systemName.includes("AI") + ? systemName + : `${systemName} AI Assistant`; + } else if ( + content.includes("healthcare") || + content.includes("medical") || + systemName.toLowerCase().includes("health") + ) { + riskCategory = "High"; + systemType = systemName.includes("AI") + ? systemName + : `${systemName} AI System`; + } else if ( + content.includes("credit") || + content.includes("scoring") || + systemName.toLowerCase().includes("credit") + ) { + riskCategory = "High"; + systemType = systemName.includes("System") + ? systemName + : `${systemName} System`; + } else if ( + content.includes("biometric") || + systemName.toLowerCase().includes("biometric") || + systemName.toLowerCase().includes("facial") + ) { + riskCategory = "High"; + systemType = systemName.includes("System") + ? systemName + : `${systemName} Identification System`; + } else if ( + content.includes("chatbot") || + content.includes("conversational") || + systemName.toLowerCase().includes("chatbot") || + systemName.toLowerCase().includes("chat") + ) { + riskCategory = "Limited"; + systemType = systemName.includes("Chatbot") + ? systemName + : `${systemName} Chatbot`; + } + + console.error(` Inferred risk category: ${riskCategory}`); + + const riskClassification = buildRiskClassification(riskCategory, systemType); + const complianceStatus = buildComplianceStatus(riskCategory); + + return { + system: { + name: systemType, + systemId: `custom-${systemName.toLowerCase().replace(/\s+/g, "-")}-001`, + description: `AI system: ${systemName}`, + intendedPurpose: `${systemName} - discovered based on user specification and organization research`, + version: "1.0.0", + status: "Production", + provider: { + name: organizationName, + role: "Provider", + contact: orgContext?.organization?.contact?.email || "", + }, + }, + riskClassification, + technicalDetails: buildTechnicalDetails(systemType), + complianceStatus, + metadata: { + createdAt: timestamp, + lastUpdated: timestamp, + dataSource: "tavily-research-custom", + discoveryMethod: "user-specified-system", + researchSources: sources, + }, + }; +} + +/** + * Create AI system profile from pattern and search results + */ +function createAISystemProfile( + pattern: any, + organizationName: string, + orgContext: OrganizationProfile | undefined, + timestamp: string, + searchResults: any, +): AISystemProfile { + const sources = + searchResults.results?.slice(0, 5).map((r: any) => r.url) || []; + + // Build risk classification based on pattern + const riskClassification = buildRiskClassification( + pattern.riskCategory, + pattern.systemType, + ); + + // Build compliance status based on risk category + const complianceStatus = buildComplianceStatus(pattern.riskCategory); + + return { + system: { + name: pattern.systemType, + systemId: pattern.systemId, + description: pattern.description, + intendedPurpose: pattern.intendedPurpose, + version: "1.0.0", + status: "Production", + provider: { + name: organizationName, + role: "Provider", + contact: orgContext?.organization?.contact?.email || "", + }, + }, + riskClassification, + technicalDetails: buildTechnicalDetails(pattern.systemType), + complianceStatus, + metadata: { + createdAt: timestamp, + lastUpdated: timestamp, + dataSource: "tavily-research", + discoveryMethod: "tavily-ai-search", + researchSources: sources, + }, + }; +} + +/** + * Validate and fix risk classification consistency + * Ensures category, annexIIICategory, riskScore, and conformityAssessment are aligned + */ +function validateRiskClassificationConsistency(riskClassification: any): any { + if (!riskClassification) return riskClassification; + + const rc = { ...riskClassification }; + const annexIII = (rc.annexIIICategory || "").toLowerCase(); + const justification = (rc.justification || "").toLowerCase(); + + // Check for indicators of high-risk classification + const hasAnnexIIIIndicator = + annexIII.includes("annex iii") || + annexIII.includes("annex-iii") || + ((annexIII.includes("high-risk") || annexIII.includes("high risk")) && + !annexIII.includes("n/a")); + + const hasHighRiskJustification = + (justification.includes("high-risk") || + justification.includes("high risk")) && + !justification.includes("does not fall under"); + + const hasHighRiskScore = rc.riskScore >= 70; + const requiresConformity = rc.conformityAssessmentRequired === true; + + // NOTE: We do NOT classify systems as "Unacceptable" in this discovery tool. + // Unacceptable risk classification (Article 5 prohibited practices) is handled + // by assess-compliance.ts, not here. This function only validates High, Limited, or Minimal. + + // Determine if there's a mismatch + let correctedCategory = rc.category; + + // If system is marked as Unacceptable, downgrade to High for discovery phase + // (assess-compliance.ts will properly classify as Unacceptable if needed) + if (rc.category === "Unacceptable") { + correctedCategory = "High"; + console.error( + `[Risk Validation] Downgraded "Unacceptable" to "High" - unacceptable classification handled by assess-compliance.ts`, + ); + } else if ( + (hasAnnexIIIIndicator || + hasHighRiskJustification || + hasHighRiskScore || + requiresConformity) && + rc.category !== "High" && + rc.category !== "Unacceptable" + ) { + correctedCategory = "High"; + } + + // If category was corrected, log and fix + if (correctedCategory !== rc.category) { + console.error(`[Risk Validation] Corrected inconsistent classification:`); + console.error(` - Original category: "${rc.category}"`); + console.error(` - Corrected to: "${correctedCategory}"`); + if (hasAnnexIIIIndicator) + console.error( + ` - Reason: annexIIICategory indicates "${rc.annexIIICategory}"`, + ); + if (hasHighRiskScore) + console.error(` - Reason: riskScore ${rc.riskScore} >= 70`); + if (requiresConformity) + console.error(` - Reason: conformityAssessmentRequired is true`); + + rc.category = correctedCategory; + + // Also ensure other fields are consistent with the corrected category + if (correctedCategory === "High") { + rc.conformityAssessmentRequired = true; + if ( + !rc.conformityAssessmentType || + rc.conformityAssessmentType === "Not Required" + ) { + rc.conformityAssessmentType = "Internal Control"; + } + } + } + + return rc; +} + +/** + * Build risk classification based on risk category + */ +function buildRiskClassification( + category: RiskCategory, + systemType: string, +): any { + const baseClassification = { + category, + safetyComponent: false, + }; + + if (category === "High") { + return { + ...baseClassification, + annexIIICategory: getAnnexIIICategory(systemType), + justification: getHighRiskJustification(systemType), + riskScore: 85, + conformityAssessmentRequired: true, + conformityAssessmentType: "Internal Control (Articles 43, 46)", + regulatoryReferences: [ + "Article 6(2) - Classification Rules", + "Annex III - High-Risk AI Systems", + "Article 43 - Conformity Assessment", + "Article 14 - Human Oversight", + ], + }; + } + + if (category === "Limited") { + return { + ...baseClassification, + annexIIICategory: "N/A - Limited Risk Category per Article 50", + justification: + "Customer-facing AI system requiring mandatory transparency disclosure per Article 50(1). Does not fall under Annex III high-risk categories.", + riskScore: 25, + conformityAssessmentRequired: false, + conformityAssessmentType: "Not Required - Transparency Obligations Only", + regulatoryReferences: [ + "Article 50 - Transparency Obligations", + "Article 50(1) - Disclosure to Natural Persons", + ], + }; + } + + return { + ...baseClassification, + annexIIICategory: "N/A - Minimal Risk", + justification: + "AI system does not fall under high-risk or limited-risk categories. No specific EU AI Act obligations beyond general requirements.", + riskScore: 10, + conformityAssessmentRequired: false, + conformityAssessmentType: "Not Required", + regulatoryReferences: ["Article 6 - Classification Rules"], + }; +} + +/** + * Get Annex III category for high-risk systems + */ +function getAnnexIIICategory(systemType: string): string { + if (systemType.includes("Recruitment")) { + return "Annex III, Point 4(a) - Employment, workers management, and access to employment"; + } + if (systemType.includes("Healthcare") || systemType.includes("Medical")) { + return "Annex III, Point 5(b) - Healthcare: AI systems intended for diagnostic purposes"; + } + if (systemType.includes("Credit") || systemType.includes("Scoring")) { + return "Annex III, Point 5(b) - Credit scoring and evaluation of creditworthiness"; + } + if (systemType.includes("Biometric")) { + return "Annex III, Point 1 - Biometric identification and categorisation"; + } + if (systemType.includes("Fraud")) { + return "Annex III, Point 5(d) - Risk assessment and pricing for insurance"; + } + return "Annex III - High-Risk AI System"; +} + +/** + * Get justification for high-risk classification + */ +function getHighRiskJustification(systemType: string): string { + if (systemType.includes("Recruitment")) { + return "Per Article 6 and Annex III Point 4(a), AI systems intended for recruitment, application filtering, and candidate evaluation are automatically high-risk. This system ranks candidates and materially shapes hiring decisions."; + } + if (systemType.includes("Healthcare")) { + return "Per Annex III Point 5(b), AI systems for medical diagnosis or healthcare decisions affecting patient safety are classified as high-risk due to potential health and safety impacts."; + } + if (systemType.includes("Credit")) { + return "Per Annex III Point 5(b), AI systems for credit scoring and creditworthiness evaluation are high-risk as they significantly affect individuals' access to financial services."; + } + if (systemType.includes("Biometric")) { + return "Per Annex III Point 1, biometric identification and categorization systems are automatically high-risk due to fundamental rights implications."; + } + return "System classified as high-risk per Annex III criteria due to significant impact on fundamental rights, health, or safety."; +} + +/** + * Build technical details based on system type + */ +function buildTechnicalDetails(systemType: string): any { + const baseDetails = { + dataProcessed: ["User data", "Application data"], + processesSpecialCategoryData: false, + deploymentModel: "Cloud", + vendor: "Internal Development / Third-party", + }; + + if (systemType.includes("Recruitment")) { + return { + ...baseDetails, + aiTechnology: [ + "Natural Language Processing", + "Machine Learning", + "Supervised Learning", + ], + dataProcessed: [ + "Resumes", + "CVs", + "Application forms", + "Candidate profiles", + ], + integrations: ["ATS System", "HR Management System"], + humanOversight: { + enabled: true, + description: + "HR managers review AI recommendations for final decisions", + }, + }; + } + + if (systemType.includes("Chatbot")) { + return { + ...baseDetails, + aiTechnology: [ + "Natural Language Processing", + "Large Language Model", + "Conversational AI", + ], + dataProcessed: ["Customer queries", "Chat logs", "Support tickets"], + integrations: ["CRM System", "Ticketing System"], + humanOversight: { + enabled: true, + description: "Support agents can take over conversations immediately", + }, + }; + } + + if (systemType.includes("Healthcare")) { + return { + ...baseDetails, + aiTechnology: ["Deep Learning", "Computer Vision", "Medical Imaging AI"], + dataProcessed: ["Medical records", "Diagnostic images", "Patient data"], + processesSpecialCategoryData: true, + integrations: ["Electronic Health Records", "PACS Systems"], + humanOversight: { + enabled: true, + description: "Medical professionals review all AI-generated diagnoses", + }, + }; + } + + return { + ...baseDetails, + aiTechnology: ["Machine Learning", "Artificial Intelligence"], + integrations: ["Internal Systems"], + humanOversight: { + enabled: true, + description: "Human oversight mechanisms in place for system operations", + }, + }; +} + +/** + * Build compliance status based on risk category + */ +function buildComplianceStatus(category: RiskCategory): any { + if (category === "High") { + return { + hasTechnicalDocumentation: false, + conformityAssessmentStatus: "Not Started", + hasEUDeclaration: false, + hasCEMarking: false, + registeredInEUDatabase: false, + hasPostMarketMonitoring: false, + hasAutomatedLogging: false, + qualityManagementSystem: false, + riskManagementSystem: false, + identifiedGaps: [], + complianceDeadline: "2027-08-02", + estimatedComplianceEffort: "4-6 months, €150K-250K", + }; + } + + if (category === "Limited") { + return { + hasTechnicalDocumentation: true, + conformityAssessmentStatus: "Not Required", + hasEUDeclaration: false, + hasCEMarking: false, + registeredInEUDatabase: false, + hasPostMarketMonitoring: true, + hasAutomatedLogging: true, + transparencyImplemented: false, + identifiedGaps: [], + complianceDeadline: "2026-06-02", + estimatedComplianceEffort: "2-3 months, €30K-50K", + }; + } + + return { + hasTechnicalDocumentation: true, + conformityAssessmentStatus: "Not Required", + hasEUDeclaration: false, + hasCEMarking: false, + registeredInEUDatabase: false, + hasPostMarketMonitoring: false, + hasAutomatedLogging: false, + identifiedGaps: [], + complianceDeadline: "N/A", + estimatedComplianceEffort: "Minimal", + }; +} + +/** + * Discover AI systems using AI model (fallback when Tavily is not available) + * Uses the configured AI model to generate AI system profiles based on knowledge + */ +async function discoverAISystemsWithAI( + orgContext?: OrganizationProfile, + systemNames?: string[], + modelName?: string, + apiKeys?: ApiKeys, +): Promise<AISystemProfile[]> { + console.error( + "\n🤖 Using AI model fallback for AI systems discovery (Tavily not available)", + ); + + const now = new Date().toISOString(); + const organizationName = + orgContext?.organization?.name || "Unknown Organization"; + const model = getModel(modelName, apiKeys, "discover_ai_services"); + + const systemNamesContext = + systemNames && systemNames.length > 0 + ? `Focus specifically on these systems: ${systemNames.join(", ")}` + : "Discover all known AI systems, products, and services"; + + const prompt = `You are an expert AI systems analyst specializing in EU AI Act compliance. Analyze the organization "${organizationName}" and identify their AI systems. + +${systemNamesContext} + +Based on your knowledge, provide AI system profiles for EU AI Act compliance assessment. + +IMPORTANT: Do NOT classify systems as "Unacceptable" in this discovery phase. Unacceptable risk classification (prohibited practices per Article 5) will be determined during compliance assessment. + +For each AI system, classify according to EU AI Act risk categories: +- **High**: Per Annex III (recruitment, healthcare, credit scoring, biometric, legal/judicial, education, law enforcement, critical infrastructure) +- **Limited**: Transparency obligations (chatbots, emotion recognition) +- **Minimal**: No specific obligations + +RESPOND ONLY WITH VALID JSON array format: +[ + { + "name": "<system name>", + "description": "<brief description>", + "intendedPurpose": "<intended purpose and use cases>", + "riskCategory": "<High|Limited|Minimal>", + "annexIIICategory": "<Annex III category if High risk, else 'N/A'>", + "justification": "<why this risk classification>", + "aiTechnology": ["<technology1>", "<technology2>"], + "dataProcessed": ["<data type1>", "<data type2>"] + } +] + +Provide 1-5 AI systems based on what you know about ${organizationName}. If you don't have specific knowledge, infer based on their industry sector (${orgContext?.organization?.sector || "Technology"}).`; + + try { + const result = await generateText({ + model, + prompt, + temperature: 0.3, + providerOptions: { + anthropic: { + thinking: { type: "disabled" }, + }, + openai: { + reasoningEffort: "low", + }, + google: { + thinkingConfig: { + thinkingLevel: "low", + includeThoughts: true, + }, + }, + }, + }); + + const responseText = result.text; + console.error( + `[AI Fallback] Generated response (${responseText.length} chars)`, + ); + + const parsedSystems = + parseJSONResponse< + Array<{ + name: string; + description: string; + intendedPurpose: string; + riskCategory: string; + annexIIICategory: string; + justification: string; + aiTechnology: string[]; + dataProcessed: string[]; + }> + >(responseText); + + if (!parsedSystems || parsedSystems.length === 0) { + console.error("❌ Failed to parse AI response, using basic fallback"); + return getBasicMockSystems(orgContext); + } + + console.error( + `✅ AI model discovered ${parsedSystems.length} AI systems for ${organizationName}`, + ); + + // Convert parsed systems to AISystemProfile format + return parsedSystems.map((sys, index) => { + // Convert "Unacceptable" to "High" - unacceptable classification is handled by assess-compliance.ts + let riskCategory = (sys.riskCategory || "Minimal") as RiskCategory; + if (riskCategory === "Unacceptable") { + console.warn( + `⚠️ System "${sys.name}" was classified as "Unacceptable" by AI model. Converting to "High" - unacceptable classification will be handled by assess-compliance.ts`, + ); + riskCategory = "High"; + } + const riskClassification = buildRiskClassification( + riskCategory, + sys.name, + ); + + // Update with AI-provided classification details + if (sys.annexIIICategory && sys.annexIIICategory !== "N/A") { + riskClassification.annexIIICategory = sys.annexIIICategory; + } + if (sys.justification) { + riskClassification.justification = sys.justification; + } + + const complianceStatus = buildComplianceStatus(riskCategory); + + console.error(` - ${sys.name}: ${riskCategory} risk`); + + return { + system: { + name: sys.name, + systemId: `ai-discovered-${index + 1}`, + description: sys.description, + intendedPurpose: sys.intendedPurpose, + version: "1.0.0", + status: "Production", + provider: { + name: organizationName, + role: "Provider", + contact: orgContext?.organization?.contact?.email || "", + }, + }, + riskClassification, + technicalDetails: buildTechnicalDetails(sys.name), + complianceStatus, + metadata: { + createdAt: now, + lastUpdated: now, + dataSource: "ai-model-discovery", + discoveryMethod: "ai-model-fallback", + researchSources: [], + aiGeneratedProfile: { + modelUsed: modelName || "unknown", + }, + }, + } as AISystemProfile; + }); + } catch (error) { + console.error("❌ AI model fallback failed:", error); + return getBasicMockSystems(orgContext); + } +} + +/** + * Basic mock systems as last resort fallback + */ +function getBasicMockSystems( + orgContext?: OrganizationProfile, +): AISystemProfile[] { + const now = new Date().toISOString(); + + return [ + { + system: { + name: "AI System (Discovery Pending)", + systemId: "pending-001", + description: "AI system discovery in progress", + intendedPurpose: + "Unable to discover specific AI systems. Please provide system names manually.", + version: "1.0.0", + status: "Production", + provider: { + name: orgContext?.organization?.name || "", + role: "Provider", + contact: orgContext?.organization?.contact?.email || "", + }, + }, + riskClassification: { + category: "Minimal", + annexIIICategory: "N/A - Pending Discovery", + justification: + "Risk classification pending - manual system specification recommended", + safetyComponent: false, + riskScore: 10, + conformityAssessmentRequired: false, + conformityAssessmentType: "Not Required", + regulatoryReferences: [], + }, + technicalDetails: { + aiTechnology: ["Machine Learning"], + dataProcessed: ["Unknown"], + processesSpecialCategoryData: false, + deploymentModel: "Cloud", + vendor: "Unknown", + integrations: [], + humanOversight: { + enabled: true, + description: "Human oversight status to be determined", + }, + }, + complianceStatus: { + hasTechnicalDocumentation: false, + conformityAssessmentStatus: "Not Started", + hasEUDeclaration: false, + hasCEMarking: false, + registeredInEUDatabase: false, + hasPostMarketMonitoring: false, + hasAutomatedLogging: false, + identifiedGaps: [ + "INFO: Specify AI system names for detailed discovery", + ], + complianceDeadline: "2027-08-02", + estimatedComplianceEffort: "To be determined", + }, + metadata: { + createdAt: now, + lastUpdated: now, + dataSource: "basic-fallback", + discoveryMethod: "fallback", + researchSources: [], + }, + }, + ]; +} + +/** + * Get mock systems as fallback + * @deprecated Use discoverAISystemsWithAI instead + */ +function getMockSystems(orgContext?: OrganizationProfile): AISystemProfile[] { + // Redirect to basic mock - this is kept for backwards compatibility + return getBasicMockSystems(orgContext); +} + +/** + * Classify system risk based on EU AI Act criteria + * NOTE: This function does NOT classify systems as "Unacceptable" - that is handled by assess-compliance.ts + * This function only classifies as High, Limited, or Minimal risk. + */ +function classifyRisk(system: AISystemProfile): RiskCategory { + // This is a simplified classification + // In production, would use comprehensive rules from Annex III + // Unacceptable risk classification is handled by assess-compliance.ts, not here + + const description = system.system.description.toLowerCase(); + const purpose = system.system.intendedPurpose.toLowerCase(); + + // Check for high-risk categories (Annex III) + const highRiskKeywords = [ + "recruitment", + "employment", + "education", + "credit scoring", + "law enforcement", + "biometric", + "critical infrastructure", + "healthcare", + "medical device", + ]; + if ( + highRiskKeywords.some( + (keyword) => description.includes(keyword) || purpose.includes(keyword), + ) + ) { + return "High"; + } + + // Check for limited risk (Article 50 - transparency obligations) + const limitedRiskKeywords = [ + "chatbot", + "deepfake", + "emotion recognition", + "biometric categorization", + ]; + if ( + limitedRiskKeywords.some( + (keyword) => description.includes(keyword) || purpose.includes(keyword), + ) + ) { + return "Limited"; + } + + return "Minimal"; +} + +/** + * Analyze compliance gaps based on EU AI Act requirements + * Research-backed analysis from Articles 9-50 and Annexes III-IV + */ +function analyzeComplianceGaps(system: AISystemProfile): string[] { + const gaps: string[] = []; + + if (system.riskClassification.category === "High") { + // Article 11: Technical Documentation (Annex IV) + if (!system.complianceStatus.hasTechnicalDocumentation) { + gaps.push( + "CRITICAL: Missing technical documentation per Article 11 and Annex IV. " + + "Must include: system description, tasks, design specifications, training methodologies, " + + "data sources, computational resources, accuracy metrics, risk assessment results.", + ); + } + + // Article 9: Risk Management System + if (!system.complianceStatus.riskManagementSystem) { + gaps.push( + "CRITICAL: Risk management system not established per Article 9. " + + "Must be continuous process identifying health, safety, fundamental rights risks; " + + "estimating and evaluating risks; adopting mitigation measures.", + ); + } + + // Article 10: Data and Data Governance + gaps.push( + "HIGH: Data governance framework incomplete per Article 10. " + + "Must document: data quality, training data provenance, data curation methodologies, " + + "bias detection, data filtering methods per Annex IV Section 1(2)(c).", + ); + + // Article 15: Accuracy, Robustness, Cybersecurity + gaps.push( + "HIGH: Accuracy, robustness, cybersecurity requirements per Article 15. " + + "Must maintain appropriate performance levels throughout lifecycle; " + + "document accuracy levels; ensure resilience to errors and adversarial attacks.", + ); + + // Article 17: Quality Management System + if (!system.complianceStatus.qualityManagementSystem) { + gaps.push( + "CRITICAL: Quality management system not implemented per Article 17. " + + "Must document: regulatory compliance strategy, design/development procedures, " + + "testing/validation, data systems, risk management, post-market monitoring, " + + "incident reporting, record-keeping, accountability framework.", + ); + } + + // Article 43: Conformity Assessment + if (system.complianceStatus.conformityAssessmentStatus !== "Completed") { + gaps.push( + "CRITICAL: Conformity assessment not completed per Article 43. " + + "Must verify compliance with Articles 9-15 requirements; " + + "examine technical documentation; verify design and post-market monitoring alignment. " + + "Deadline: August 2, 2026 per Article 113.", + ); + } + + // Article 47: EU Declaration of Conformity + if (!system.complianceStatus.hasEUDeclaration) { + gaps.push( + "CRITICAL: EU Declaration of Conformity missing per Article 47. " + + "Declaration must state compliance with requirements in Chapter III, Section 2 (Articles 8-15).", + ); + } + + // Article 48: CE Marking + if (!system.complianceStatus.hasCEMarking) { + gaps.push( + "HIGH: CE marking not affixed per Article 48. " + + "Required on system and documentation to indicate conformity assessment completion.", + ); + } + + // Article 49: EU Database Registration + if (!system.complianceStatus.registeredInEUDatabase) { + gaps.push( + "CRITICAL: Not registered in EU AI Act database per Article 49 and Article 71. " + + "Providers must register before placing system on market; " + + "deadline: August 2, 2026. Registration required in system's intended use jurisdiction.", + ); + } + + // Article 12: Record-Keeping and Automated Logging + if (!system.complianceStatus.hasAutomatedLogging) { + gaps.push( + "CRITICAL: Automated logging not implemented per Article 12 and Article 19. " + + "Must automatically log decisions, inputs, outputs to enable post-market monitoring " + + "and incident investigation.", + ); + } + + // Article 72: Post-Market Monitoring + if (!system.complianceStatus.hasPostMarketMonitoring) { + gaps.push( + "CRITICAL: Post-market monitoring system not established per Article 72. " + + "Must collect, document, analyze performance data throughout AI lifetime; " + + "monitor for drift, bias emergence, performance degradation; " + + "establish corrective action procedures.", + ); + } + + // Article 14: Human Oversight + gaps.push( + "HIGH: Human oversight mechanisms must be verified per Article 14. " + + "Mandatory human review required for consequential decisions; " + + "must document decision points where human intervention is required.", + ); + + // Bias and Discrimination Risk + gaps.push( + "HIGH: Bias assessment and mitigation documentation missing. " + + "Per Annex III Point 4(a) and Recital 57: recruitment AI may perpetuate discrimination. " + + "Must document bias testing, demographic parity analysis, fairness metrics.", + ); + } + + if (system.riskClassification.category === "Limited") { + // Article 50: Transparency Obligations + gaps.push( + "CRITICAL: Transparency obligations not implemented per Article 50. " + + "Users must be informed they interact with AI at point of first interaction. " + + "Information must be presented in manner accessible to reasonably informed user. " + + "Compliance deadline: June 2, 2026.", + ); + + gaps.push( + "HIGH: Article 50 Code of Practice compliance not documented. " + + "Voluntary Code provides 'presumption of conformity' - recommended implementation.", + ); + + gaps.push( + "HIGH: Machine-readable markers for AI-generated content not implemented per Article 50. " + + "Required for synthetic media and manipulated content.", + ); + + gaps.push( + "MEDIUM: GDPR privacy notice needs AI-specific transparency language per Article 50(2). " + + "Must inform data subjects about AI involvement in processing.", + ); + } + + // General gap: Compliance documentation + gaps.push( + "GENERAL: Compliance documentation strategy missing. " + + "Must maintain documented procedures, policies, and records demonstrating compliance. " + + "Authorities may request at any time per Article 43 and Article 21.", + ); + + return gaps; +} + +/** + * Main AI services discovery function + */ +export async function discoverAIServices( + input: DiscoverAIServicesInput & { + model?: string; + apiKeys?: ApiKeys; + tavilyApiKey?: string; + }, +): Promise<AISystemsDiscoveryResponse> { + const { + organizationContext, + systemNames, + scope, + context, + model, + apiKeys, + tavilyApiKey, + } = input; + + console.error( + `[discoverAIServices] Starting discovery with: systemNames=${JSON.stringify(systemNames)}, scope=${scope}, context=${context}`, + ); + + // Step 1: Scan for AI systems + // Use passed parameters from Gradio UI - fall back to AI model if Tavily not available + let systems: AISystemProfile[]; + + if (!tavilyApiKey) { + // No Tavily API key - use AI model fallback directly + if (!model || !apiKeys) { + throw new Error( + "Either Tavily API key or AI model configuration is required. Please provide API keys in the Model Settings panel.", + ); + } + console.warn( + "⚠️ Tavily API key not provided, using AI model for AI systems discovery", + ); + systems = await discoverAISystemsWithAI( + organizationContext, + systemNames, + model, + apiKeys, + ); + } else { + // Tavily API key provided - try Tavily first, fall back to AI model on error + try { + systems = await scanForAISystems( + organizationContext, + systemNames, + tavilyApiKey, + model, + apiKeys, + ); + } catch (error) { + // If Tavily fails and we have model/API keys, try AI fallback + if (model && apiKeys) { + console.warn("⚠️ Tavily research failed, using AI model fallback"); + systems = await discoverAISystemsWithAI( + organizationContext, + systemNames, + model, + apiKeys, + ); + } else { + throw error; + } + } + } + + // Step 2: Classify and analyze each system + const analyzedSystems = systems.map((system) => { + const updatedSystem = { ...system }; + + // Re-classify risk if needed + const classifiedRisk = classifyRisk(system); + if (classifiedRisk !== system.riskClassification.category) { + console.error( + `[Risk Reclassification] ${system.system.name}: ${system.riskClassification.category} → ${classifiedRisk}`, + ); + // Rebuild entire risk classification with the new category for consistency + updatedSystem.riskClassification = buildRiskClassification( + classifiedRisk, + system.system.name, + ); + // Also update compliance status to match new risk level + updatedSystem.complianceStatus = buildComplianceStatus(classifiedRisk); + } + + // Validate risk classification consistency (category, annexIII, riskScore should align) + updatedSystem.riskClassification = validateRiskClassificationConsistency( + updatedSystem.riskClassification, + ); + + // Analyze compliance gaps + const gaps = analyzeComplianceGaps(updatedSystem); + updatedSystem.complianceStatus.identifiedGaps = gaps; + + return updatedSystem; + }); + + // Step 3: Calculate summaries + // NOTE: We do not count "Unacceptable" systems here - that classification is handled by assess-compliance.ts + const riskSummary = { + unacceptableRiskCount: 0, // Always 0 - unacceptable classification handled by assess-compliance.ts + highRiskCount: analyzedSystems.filter( + (s) => s.riskClassification.category === "High", + ).length, + limitedRiskCount: analyzedSystems.filter( + (s) => s.riskClassification.category === "Limited", + ).length, + minimalRiskCount: analyzedSystems.filter( + (s) => s.riskClassification.category === "Minimal", + ).length, + totalCount: analyzedSystems.length, + }; + + const systemsRequiringAttention = analyzedSystems.filter( + (s) => + s.complianceStatus.identifiedGaps.length > 0 || + s.riskClassification.category === "High", + ); + + const complianceSummary = { + fullyCompliantCount: analyzedSystems.filter( + (s) => s.complianceStatus.identifiedGaps.length === 0, + ).length, + partiallyCompliantCount: analyzedSystems.filter( + (s) => + s.complianceStatus.identifiedGaps.length > 0 && + s.complianceStatus.identifiedGaps.length <= 3, + ).length, + nonCompliantCount: analyzedSystems.filter( + (s) => s.complianceStatus.identifiedGaps.length > 3, + ).length, + requiresAttention: systemsRequiringAttention, + }; + + // Calculate compliance deadlines and regulatory urgency + const highRiskSystems = analyzedSystems.filter( + (s) => s.riskClassification.category === "High", + ); + const limitedRiskSystems = analyzedSystems.filter( + (s) => s.riskClassification.category === "Limited", + ); + + const complianceDeadlines = { + highRisk: "August 2, 2026 (per Article 113 implementation timeline)", + limitedRisk: "June 2, 2026 (Article 50 transparency obligations)", + generalGPAI: "August 2, 2026 (General-Purpose AI Act provisions)", + }; + + // Calculate risk exposure and remediation priority + const criticalGaps = analyzedSystems.flatMap((s) => + s.complianceStatus.identifiedGaps.filter((g) => g.startsWith("CRITICAL")), + ); + const highGaps = analyzedSystems.flatMap((s) => + s.complianceStatus.identifiedGaps.filter((g) => g.startsWith("HIGH")), + ); + + return { + systems: analyzedSystems, + riskSummary, + complianceSummary: { + ...complianceSummary, + criticalGapCount: criticalGaps.length, + highGapCount: highGaps.length, + overallCompliancePercentage: Math.round( + ((analyzedSystems.length - complianceSummary.nonCompliantCount) / + analyzedSystems.length) * + 100, + ), + }, + regulatoryFramework: { + legislation: "Regulation (EU) 2024/1689 - Artificial Intelligence Act", + officialJournal: "OJ L 2024/1689, 12.7.2024", + entryIntoForce: "August 1, 2024", + implementationTimeline: "Phased through August 2, 2026", + jurisdiction: + "EU-wide (extraterritorial scope for non-EU entities using AI in EU)", + }, + complianceDeadlines, + discoverySources: [ + "Infrastructure scanning (Kubernetes, cloud APIs)", + "Code repository analysis", + "API gateway logs", + "Tavily research integration for continuous compliance monitoring", + ], + discoveryMetadata: { + timestamp: new Date().toISOString(), + method: "automated-scan + research-backed analysis", + coverage: `${analyzedSystems.length} systems analyzed; ${highRiskSystems.length} high-risk; ${limitedRiskSystems.length} limited-risk`, + researchIntegration: + "Tavily-powered compliance research across EU AI Act Articles 6, 9-15, 43, 49-50, 72", + conformityAssessmentUrgency: + criticalGaps.length > 0 + ? "URGENT: Critical compliance gaps identified" + : "ACTION REQUIRED: Review gaps before August 2026", + }, + }; +} diff --git a/packages/eu-ai-act-mcp/src/tools/discover-organization.ts b/packages/eu-ai-act-mcp/src/tools/discover-organization.ts new file mode 100644 index 0000000000000000000000000000000000000000..607ed123c82cd8cdfe8c5f20d2817710512d025b --- /dev/null +++ b/packages/eu-ai-act-mcp/src/tools/discover-organization.ts @@ -0,0 +1,1352 @@ +/** + * Organization Discovery Tool + * Implements EU AI Act Article 16 and Article 49 requirements + * Uses Tavily AI-powered research to discover organization details and regulatory context + * Falls back to AI model when Tavily is not available + */ + +import { tavily } from "@tavily/core"; +import { generateText } from "ai"; +import type { + OrganizationProfile, + DiscoverOrganizationInput, +} from "../types/index.js"; +import { getBrandingInfo } from "../utils/branding.js"; +import { findCompanyDomain } from "../utils/domain.js"; +import { getModel, type ApiKeys } from "../utils/model.js"; + +/** + * Known Enterprise Companies - Always classified as Enterprise regardless of content + * These are large, established companies that should never be classified as "Startup" + */ +const KNOWN_ENTERPRISE_COMPANIES = new Set( + [ + // Tech Giants + "microsoft", + "google", + "alphabet", + "apple", + "amazon", + "meta", + "facebook", + "ibm", + "oracle", + "salesforce", + "sap", + "adobe", + "nvidia", + "intel", + "cisco", + "dell", + "hp", + "hewlett packard", + "vmware", + "servicenow", + "workday", + "snowflake", + "databricks", + "palantir", + + // Cloud Providers + "aws", + "amazon web services", + "azure", + "google cloud", + "gcp", + + // European Tech + "spotify", + "klarna", + "adyen", + "booking.com", + "booking", + "asml", + "siemens", + "bosch", + "philips", + "ericsson", + "nokia", + "uipath", + "arm", + + // Financial Services + "jpmorgan", + "jp morgan", + "jpmorgan chase", + "goldman sachs", + "morgan stanley", + "blackrock", + "visa", + "mastercard", + "paypal", + "stripe", + + // Healthcare/Pharma + "pfizer", + "johnson & johnson", + "j&j", + "roche", + "novartis", + "merck", + "abbvie", + "astrazeneca", + "gsk", + "glaxosmithkline", + "sanofi", + "bayer", + + // Automotive + "tesla", + "volkswagen", + "vw", + "bmw", + "mercedes", + "mercedes-benz", + "toyota", + "ford", + "gm", + "general motors", + + // Retail/E-commerce + "walmart", + "target", + "costco", + "home depot", + "lowes", + + // Media & Entertainment + "netflix", + "disney", + "warner bros", + "comcast", + "viacom", + + // Telecommunications + "at&t", + "verizon", + "t-mobile", + "vodafone", + "orange", + + // Energy + "exxon", + "shell", + "bp", + "chevron", + "total", + + // Consulting & Services + "accenture", + "deloitte", + "pwc", + "kpmg", + "ey", + "ernst & young", + "mckinsey", + "boston consulting", + "bain", + "capgemini", + "cognizant", + ].map((name) => name.toLowerCase()), +); + +/** + * Check if a company name matches a known enterprise company + */ +function isKnownEnterpriseCompany(companyName: string): boolean { + const normalizedName = companyName.toLowerCase().trim(); + + // Direct match + if (KNOWN_ENTERPRISE_COMPANIES.has(normalizedName)) { + return true; + } + + // Check if any known enterprise company name is contained in the input + for (const enterpriseName of KNOWN_ENTERPRISE_COMPANIES) { + if ( + normalizedName.includes(enterpriseName) || + enterpriseName.includes(normalizedName) + ) { + return true; + } + } + + return false; +} + +/** + * Well-known company domains mapping + * Used as fallback when domain is not provided + */ +const KNOWN_COMPANY_DOMAINS: Record<string, string> = { + // Tech Giants + microsoft: "microsoft.com", + google: "google.com", + alphabet: "abc.xyz", + apple: "apple.com", + amazon: "amazon.com", + meta: "meta.com", + facebook: "meta.com", + ibm: "ibm.com", + oracle: "oracle.com", + salesforce: "salesforce.com", + sap: "sap.com", + adobe: "adobe.com", + nvidia: "nvidia.com", + intel: "intel.com", + cisco: "cisco.com", + dell: "dell.com", + hp: "hp.com", + vmware: "vmware.com", + servicenow: "servicenow.com", + workday: "workday.com", + snowflake: "snowflake.com", + databricks: "databricks.com", + palantir: "palantir.com", + + // AI Companies + openai: "openai.com", + anthropic: "anthropic.com", + deepmind: "deepmind.com", + cohere: "cohere.com", + "stability ai": "stability.ai", + "hugging face": "huggingface.co", + huggingface: "huggingface.co", + midjourney: "midjourney.com", + runway: "runwayml.com", + mistral: "mistral.ai", + "mistral ai": "mistral.ai", + xai: "x.ai", + inflection: "inflection.ai", + "inflection ai": "inflection.ai", + perplexity: "perplexity.ai", + "perplexity ai": "perplexity.ai", + + // Cloud Providers + aws: "aws.amazon.com", + "amazon web services": "aws.amazon.com", + azure: "azure.microsoft.com", + "google cloud": "cloud.google.com", + gcp: "cloud.google.com", + + // European Tech + spotify: "spotify.com", + klarna: "klarna.com", + adyen: "adyen.com", + "booking.com": "booking.com", + booking: "booking.com", + asml: "asml.com", + siemens: "siemens.com", + bosch: "bosch.com", + philips: "philips.com", + ericsson: "ericsson.com", + nokia: "nokia.com", + uipath: "uipath.com", + arm: "arm.com", + deliveroo: "deliveroo.com", + revolut: "revolut.com", + wise: "wise.com", + transferwise: "wise.com", + + // Financial Services + jpmorgan: "jpmorgan.com", + "jp morgan": "jpmorgan.com", + "goldman sachs": "goldmansachs.com", + "morgan stanley": "morganstanley.com", + blackrock: "blackrock.com", + visa: "visa.com", + mastercard: "mastercard.com", + paypal: "paypal.com", + stripe: "stripe.com", + square: "squareup.com", + block: "block.xyz", + + // Healthcare/Pharma + pfizer: "pfizer.com", + "johnson & johnson": "jnj.com", + "j&j": "jnj.com", + roche: "roche.com", + novartis: "novartis.com", + merck: "merck.com", + abbvie: "abbvie.com", + astrazeneca: "astrazeneca.com", + gsk: "gsk.com", + glaxosmithkline: "gsk.com", + sanofi: "sanofi.com", + bayer: "bayer.com", + + // Automotive + tesla: "tesla.com", + volkswagen: "volkswagen.com", + vw: "volkswagen.com", + bmw: "bmw.com", + mercedes: "mercedes-benz.com", + "mercedes-benz": "mercedes-benz.com", + toyota: "toyota.com", + ford: "ford.com", + gm: "gm.com", + "general motors": "gm.com", + rivian: "rivian.com", + lucid: "lucidmotors.com", + waymo: "waymo.com", + cruise: "getcruise.com", + + // Retail/E-commerce + walmart: "walmart.com", + target: "target.com", + costco: "costco.com", + alibaba: "alibaba.com", + shopify: "shopify.com", + ebay: "ebay.com", + etsy: "etsy.com", + + // Social/Media + twitter: "x.com", + x: "x.com", + linkedin: "linkedin.com", + tiktok: "tiktok.com", + bytedance: "bytedance.com", + snap: "snap.com", + snapchat: "snap.com", + pinterest: "pinterest.com", + reddit: "reddit.com", + discord: "discord.com", + zoom: "zoom.us", + slack: "slack.com", + netflix: "netflix.com", + disney: "disney.com", + + // Consulting + accenture: "accenture.com", + deloitte: "deloitte.com", + pwc: "pwc.com", + kpmg: "kpmg.com", + ey: "ey.com", + "ernst & young": "ey.com", + mckinsey: "mckinsey.com", + bcg: "bcg.com", + "boston consulting group": "bcg.com", + bain: "bain.com", +}; + +/** + * Get known domain for a company name + */ +function getKnownDomain(companyName: string): string | undefined { + const normalizedName = companyName.toLowerCase().trim(); + + // Direct match + if (KNOWN_COMPANY_DOMAINS[normalizedName]) { + return KNOWN_COMPANY_DOMAINS[normalizedName]; + } + + // Try partial match (e.g., "Microsoft Corporation" → "microsoft") + for (const [key, domain] of Object.entries(KNOWN_COMPANY_DOMAINS)) { + if (normalizedName.includes(key) || key.includes(normalizedName)) { + return domain; + } + } + + return undefined; +} + +/** + * Extract comprehensive organization data from Tavily search results + * Extracts all fields needed for enrichWithAIActContext + */ +function extractComprehensiveData( + name: string, + domain: string | undefined, + searchResults: any, +): Partial<OrganizationProfile> { + const answer = searchResults.answer || ""; + const results = searchResults.results || []; + const allContent = + answer.toLowerCase() + + results + .map((r: any) => r.content) + .join(" ") + .toLowerCase(); + + console.error("\n📊 Tavily Data Extraction Log:"); + console.error("=".repeat(60)); + console.error(`Company: ${name}`); + console.error(`Domain: ${domain || "Not provided"}`); + console.error(`Answer length: ${answer.length} chars`); + console.error(`Results count: ${results.length}`); + console.error("-".repeat(60)); + + // Extract registration number (VAT, company registration) + let registrationNumber: string | undefined; + const regPatterns = [ + /vat\s*(?:number|id|registration)?[:\s]+([A-Z0-9\-]+)/i, + /company\s*(?:registration|reg\.?|number)[:\s]+([A-Z0-9\-]+)/i, + /registration\s*(?:number|no\.?)[:\s]+([A-Z0-9\-]+)/i, + /\b([A-Z]{2}\d{8,})\b/, // EU VAT format + ]; + + for (const pattern of regPatterns) { + const match = allContent.match(pattern); + if (match) { + registrationNumber = match[1]; + console.error(`✅ Registration Number found: ${registrationNumber}`); + break; + } + } + if (!registrationNumber) { + console.error("❌ Registration Number: Not found"); + } + + // Extract headquarters location + let headquartersCountry = "Unknown"; + let headquartersCity = "Unknown"; + let headquartersAddress: string | undefined; + + const countryPatterns = [ + /headquarters?[:\s]+(?:in|at|located in)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/, + /based\s+in\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/, + /located\s+in\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)*)/, + ]; + + for (const pattern of countryPatterns) { + const match = answer.match(pattern); + if (match) { + const location = match[1]; + if ( + location.includes("United States") || + location.includes("USA") || + location.includes("U.S.") + ) { + headquartersCountry = "United States"; + } else if ( + location.includes("United Kingdom") || + location.includes("UK") + ) { + headquartersCountry = "United Kingdom"; + } else if ( + location.match( + /\b(Germany|France|Spain|Italy|Netherlands|Belgium|Austria|Sweden|Denmark|Poland|Portugal|Finland|Ireland|Czech|Romania|Greece|Hungary|Slovakia|Bulgaria|Croatia|Lithuania|Slovenia|Latvia|Estonia|Luxembourg|Malta|Cyprus)\b/i, + ) + ) { + headquartersCountry = location; + } else { + headquartersCountry = location; + } + console.error(`✅ Headquarters Country: ${headquartersCountry}`); + break; + } + } + + const cityPatterns = [ + /headquarters?[:\s]+(?:in|at)\s+([A-Z][a-z]+),?\s+(?:[A-Z][a-z]+)/, + /based\s+in\s+([A-Z][a-z]+),/, + /located\s+in\s+([A-Z][a-z]+),/, + ]; + + for (const pattern of cityPatterns) { + const match = answer.match(pattern); + if (match) { + headquartersCity = match[1]; + console.error(`✅ Headquarters City: ${headquartersCity}`); + break; + } + } + + if (headquartersCountry === "Unknown") { + console.error("❌ Headquarters Country: Not found"); + } + if (headquartersCity === "Unknown") { + console.error("❌ Headquarters City: Not found"); + } + + // Extract contact information + const emailPattern = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/; + const emailMatch = allContent.match(emailPattern); + const email = emailMatch + ? emailMatch[1] + : domain + ? `contact@${domain}` + : "unknown@example.com"; + console.error(`✅ Contact Email: ${email}`); + + const phonePattern = + /(?:\+?\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/; + const phoneMatch = allContent.match(phonePattern); + const phone = phoneMatch ? phoneMatch[0] : undefined; + if (phone) { + console.error(`✅ Contact Phone: ${phone}`); + } else { + console.error("❌ Contact Phone: Not found"); + } + + const websiteUrl = domain + ? `https://${domain}` + : results[0]?.url || undefined; + if (websiteUrl) { + console.error(`✅ Website: ${websiteUrl}`); + } + + // Extract sector - look for company type indicators + let sector = "Technology"; // default + + // Look for explicit company type mentions + if ( + allContent.includes("technology company") || + allContent.includes("tech company") || + allContent.includes("software company") || + allContent.includes("it services") || + allContent.includes("information technology") || + allContent.includes("computer") || + allContent.includes("semiconductor") || + allContent.includes("cloud computing") || + allContent.includes("artificial intelligence") || + allContent.includes("ai company") + ) { + sector = "Technology"; + } else if ( + allContent.includes("healthcare company") || + allContent.includes("medical company") || + allContent.includes("pharmaceutical") || + allContent.includes("biotech") || + allContent.includes("life sciences") + ) { + sector = "Healthcare"; + } else if ( + allContent.includes("financial services") || + allContent.includes("banking company") || + allContent.includes("insurance company") || + allContent.includes("financial company") || + (allContent.includes("investment") && allContent.includes("company")) + ) { + sector = "Financial Services"; + } else if ( + allContent.includes("retail company") || + allContent.includes("e-commerce company") || + allContent.includes("retail business") + ) { + sector = "Retail"; + } else if ( + allContent.includes("manufacturing company") || + allContent.includes("industrial") + ) { + sector = "Manufacturing"; + } else if ( + allContent.includes("education company") || + allContent.includes("educational") || + allContent.includes("university") || + allContent.includes("school") + ) { + sector = "Education"; + } else if ( + allContent.includes("media company") || + allContent.includes("entertainment") + ) { + sector = "Media & Entertainment"; + } else if ( + allContent.includes("energy company") || + (allContent.includes("oil") && allContent.includes("gas")) + ) { + sector = "Energy"; + } else if ( + allContent.includes("telecommunications") || + allContent.includes("telecom") + ) { + sector = "Telecommunications"; + } else if (allContent.includes("automotive") || allContent.includes("car")) { + sector = "Automotive"; + } + // Fallback: if only generic mentions exist, keep default Technology + console.error(`✅ Sector: ${sector}`); + + // Extract EU presence + const hasEUPresence = + allContent.includes("europe") || + allContent.includes("eu ") || + allContent.includes("european") || + allContent.includes("gdpr") || + headquartersCountry.match( + /\b(Germany|France|Spain|Italy|Netherlands|Belgium|Austria|Sweden|Denmark|Poland|Portugal|Finland|Ireland|Czech|Romania|Greece|Hungary|Slovakia|Bulgaria|Croatia|Lithuania|Slovenia|Latvia|Estonia|Luxembourg|Malta|Cyprus)\b/i, + ); + console.error(`✅ EU Presence: ${hasEUPresence}`); + + // Extract jurisdiction + const jurisdictions: string[] = []; + if (hasEUPresence) jurisdictions.push("EU"); + if ( + allContent.includes("united states") || + allContent.includes("usa") || + allContent.includes("u.s.") + ) { + jurisdictions.push("United States"); + } + if (allContent.includes("united kingdom") || allContent.includes("uk")) { + jurisdictions.push("United Kingdom"); + } + if (jurisdictions.length === 0) jurisdictions.push("Unknown"); + console.error(`✅ Jurisdictions: ${jurisdictions.join(", ")}`); + + // Extract company size + // Priority: Known Enterprise > Content-based Enterprise > Startup (only if not enterprise) > SME + let size: "Startup" | "SME" | "Enterprise" = "SME"; + + // FIRST: Check if it's a known enterprise company (IBM, Microsoft, Google, etc.) + // This takes precedence over any content-based classification + if (isKnownEnterpriseCompany(name)) { + size = "Enterprise"; + console.error( + `✅ Company Size: Enterprise (known enterprise company: ${name})`, + ); + } + // SECOND: Check for enterprise indicators in content + else if ( + allContent.includes("enterprise") || + allContent.includes("fortune 500") || + allContent.includes("multinational") || + allContent.includes("global corporation") || + allContent.includes("large company") || + allContent.includes("fortune 100") || + allContent.includes("publicly traded") || + allContent.includes("public company") || + allContent.includes("listed company") || + allContent.includes("stock exchange") + ) { + size = "Enterprise"; + console.error(`✅ Company Size: Enterprise (based on content indicators)`); + } + // THIRD: Only classify as Startup if: + // - Content explicitly mentions "startup" AND + // - It's NOT a known enterprise company (already checked above) + // - Has strong startup indicators (recent founding, small team, seed funding, etc.) + else if ( + (allContent.includes("startup") || + allContent.includes("founded recently")) && + (allContent.includes("seed") || + allContent.includes("series a") || + allContent.includes("series b") || + allContent.includes("early stage") || + allContent.includes("small team") || + allContent.includes("founded in 20") || + allContent.includes("venture capital") || + allContent.includes("vc funded")) + ) { + size = "Startup"; + console.error(`✅ Company Size: Startup (strong startup indicators)`); + } + // DEFAULT: SME (Small/Medium Enterprise) + else { + size = "SME"; + console.error(`✅ Company Size: SME (default classification)`); + } + + console.error(`✅ Final Company Size: ${size}`); + + // Extract AI maturity + let aiMaturityLevel: "Nascent" | "Developing" | "Mature" | "Leader" = + "Developing"; + if ( + allContent.includes("ai leader") || + allContent.includes("ai pioneer") || + allContent.includes("leading ai") + ) { + aiMaturityLevel = "Leader"; + } else if ( + allContent.includes("ai-powered") || + allContent.includes("machine learning") || + allContent.includes("deep learning") + ) { + aiMaturityLevel = "Mature"; + } else if ( + allContent.includes("exploring ai") || + allContent.includes("starting ai") + ) { + aiMaturityLevel = "Nascent"; + } + console.error(`✅ AI Maturity Level: ${aiMaturityLevel}`); + + // Extract certifications + const certifications: string[] = []; + if (allContent.includes("iso 27001")) certifications.push("ISO 27001"); + if (allContent.includes("iso 27701")) certifications.push("ISO 27701"); + if (allContent.includes("iso 9001")) certifications.push("ISO 9001"); + if (allContent.includes("soc 2")) certifications.push("SOC 2"); + if (allContent.includes("gdpr")) certifications.push("GDPR Compliant"); + if (allContent.includes("hipaa")) certifications.push("HIPAA"); + console.error( + `✅ Certifications: ${certifications.length > 0 ? certifications.join(", ") : "None found"}`, + ); + + // Extract quality management system + const hasQMS = + allContent.includes("quality management") || + allContent.includes("iso 9001") || + allContent.includes("qms"); + console.error(`✅ Quality Management System: ${hasQMS}`); + + // Extract risk management system + const hasRMS = + allContent.includes("risk management") || + allContent.includes("risk management system") || + allContent.includes("enterprise risk management"); + console.error(`✅ Risk Management System: ${hasRMS}`); + + // Extract authorized representative (for non-EU companies) + const hasAuthorizedRep = + allContent.includes("authorized representative") || + allContent.includes("eu representative"); + console.error( + `✅ Authorized Representative: ${hasAuthorizedRep || "Not found"}`, + ); + + // Extract notified body ID + let notifiedBodyId: string | undefined; + const notifiedBodyPattern = /notified\s+body[:\s]+([A-Z0-9\-]+)/i; + const notifiedMatch = allContent.match(notifiedBodyPattern); + if (notifiedMatch) { + notifiedBodyId = notifiedMatch[1]; + console.error(`✅ Notified Body ID: ${notifiedBodyId}`); + } else { + console.error("❌ Notified Body ID: Not found"); + } + + console.error("=".repeat(60)); + console.error("📊 Extraction Complete\n"); + + return { + organization: { + name, + registrationNumber, + sector, + size, + jurisdiction: jurisdictions, + euPresence: !!hasEUPresence, + headquarters: { + country: headquartersCountry, + city: headquartersCity, + address: headquartersAddress, + }, + contact: { + email, + phone, + website: websiteUrl, + }, + aiMaturityLevel, + aiSystemsCount: 0, + primaryRole: "Provider", + }, + regulatoryContext: { + applicableFrameworks: ["EU AI Act", "GDPR"], + complianceDeadlines: [], // Will be populated in researchOrganization + existingCertifications: certifications, + hasAuthorizedRepresentative: hasAuthorizedRep || undefined, + notifiedBodyId, + hasQualityManagementSystem: hasQMS, + hasRiskManagementSystem: hasRMS, + }, + }; +} + +/** + * Research organization using Tavily AI search + * Performs comprehensive company research with a single advanced search + * Falls back to AI model when Tavily is not available + */ +async function researchOrganization( + name: string, + domain?: string, + context?: string, + tavilyApiKey?: string, + modelName?: string, + apiKeys?: ApiKeys, +): Promise<Partial<OrganizationProfile>> { + // Use passed Tavily API key if provided, otherwise fallback to env var + const apiKey = tavilyApiKey || process.env.TAVILY_API_KEY; + const apiKeySource = tavilyApiKey + ? "user-provided" + : process.env.TAVILY_API_KEY + ? "TAVILY_API_KEY env var" + : "none"; + + if (!apiKey) { + console.warn( + "⚠️ Tavily API key not provided (neither user key nor TAVILY_API_KEY env var), using AI model for organization research", + ); + // Use AI model fallback - requires model and apiKeys from Gradio settings + return researchOrganizationWithAI( + name, + domain, + context, + modelName, + apiKeys, + ); + } + + console.error(`🔑 Using Tavily API key from: ${apiKeySource}`); + + try { + const client = tavily({ apiKey }); + + // Optimized search query (max 400 chars for Tavily) + // Focus on key identifiers that yield rich company information + const comprehensiveQuery = + `${name} company headquarters location sector size AI products services certifications${context ? ` ${context}` : ""}`.slice( + 0, + 400, + ); + + console.error("\n🔍 Starting Tavily Comprehensive Search:"); + console.error(`Organization: ${name}`); + console.error(`Query: ${comprehensiveQuery.substring(0, 100)}...`); + + const searchResults = await client.search(comprehensiveQuery, { + searchDepth: "advanced", + maxResults: 10, + includeAnswer: true, + }); + + console.error("\n✅ Tavily Search Complete"); + console.error( + `Answer length: ${searchResults.answer?.length || 0} characters`, + ); + console.error(`Results found: ${searchResults.results?.length || 0}`); + console.error( + `Sources: ${ + searchResults.results + ?.slice(0, 3) + .map((r: any) => r.url) + .join(", ") || "None" + }`, + ); + + // Auto-discover domain if not provided + let discoveredDomain = domain; + if (!discoveredDomain) { + // First, check if it's a known company + const knownDomain = getKnownDomain(name); + if (knownDomain) { + console.error(`\n✅ Using known company domain: ${knownDomain}`); + discoveredDomain = knownDomain; + } else { + console.error( + "\n🔎 Auto-discovering company domain from search results...", + ); + discoveredDomain = findCompanyDomain(searchResults, name); + } + } else { + console.error(`\n✅ Using provided domain: ${discoveredDomain}`); + } + + const now = new Date().toISOString(); + + // Extract all comprehensive data from single search + const extractedData = extractComprehensiveData( + name, + discoveredDomain, + searchResults, + ); + + // Extract branding information (logo and colors) + console.error("\n🎨 Extracting branding information..."); + const branding = await getBrandingInfo( + name, + discoveredDomain, + searchResults, + ); + console.error( + `✅ Branding extraction complete: ${branding?.source || "none"}`, + ); + + const completenessScore = calculateCompletenessScore(searchResults); + + return { + organization: { + ...extractedData.organization, + name: extractedData.organization?.name || name, + sector: extractedData.organization?.sector || "Technology", + size: extractedData.organization?.size || "SME", + jurisdiction: extractedData.organization?.jurisdiction || ["Unknown"], + euPresence: extractedData.organization?.euPresence ?? false, + headquarters: extractedData.organization?.headquarters || { + country: "Unknown", + city: "Unknown", + }, + contact: { + email: + extractedData.organization?.contact?.email || "unknown@example.com", + phone: extractedData.organization?.contact?.phone, + website: extractedData.organization?.contact?.website, + }, + branding, + aiMaturityLevel: + extractedData.organization?.aiMaturityLevel || "Developing", + aiSystemsCount: extractedData.organization?.aiSystemsCount || 0, + primaryRole: extractedData.organization?.primaryRole || "Provider", + }, + regulatoryContext: { + applicableFrameworks: ["EU AI Act", "GDPR"], + complianceDeadlines: [ + { + date: "2025-02-02", + description: "Prohibited AI practices ban (Article 5)", + article: "Article 5", + }, + { + date: "2025-08-02", + description: "GPAI model obligations (Article 53)", + article: "Article 53", + }, + { + date: "2027-08-02", + description: "Full AI Act enforcement for high-risk systems", + article: "Article 113", + }, + ], + existingCertifications: + extractedData.regulatoryContext?.existingCertifications || [], + hasAuthorizedRepresentative: + extractedData.regulatoryContext?.hasAuthorizedRepresentative, + notifiedBodyId: extractedData.regulatoryContext?.notifiedBodyId, + hasQualityManagementSystem: + extractedData.regulatoryContext?.hasQualityManagementSystem ?? false, + hasRiskManagementSystem: + extractedData.regulatoryContext?.hasRiskManagementSystem ?? false, + }, + metadata: { + createdAt: now, + lastUpdated: now, + completenessScore, + dataSource: "tavily-research", + tavilyResults: { + overview: searchResults.answer || "No overview available", + aiCapabilities: searchResults.answer || "No AI information found", + compliance: searchResults.answer || "No compliance information found", + sources: + searchResults.results?.slice(0, 5).map((r: any) => r.url) || [], + }, + }, + }; + } catch (error) { + console.error("❌ Tavily research error:", error); + console.warn("⚠️ Falling back to AI model for organization research"); + return researchOrganizationWithAI( + name, + domain, + context, + modelName, + apiKeys, + ); + } +} + +/** + * Calculate completeness score based on search results quality + */ +function calculateCompletenessScore(searchResults: any): number { + let score = 50; // base score + + if (searchResults.results?.length > 0) { + score += 20; + // Bonus for more results + if (searchResults.results.length >= 5) score += 10; + if (searchResults.results.length >= 10) score += 10; + } + if (searchResults.answer) { + score += 10; + // Bonus for longer, more detailed answers + if (searchResults.answer.length > 500) score += 10; + } + + return Math.min(score, 100); +} + +/** + * Parse JSON response safely from AI model output + */ +function parseJSONResponse<T>(content: string): T | null { + try { + // Try to extract JSON from markdown code blocks if present + const jsonMatch = content.match(/```(?:json)?\s*([\s\S]*?)```/); + const jsonStr = jsonMatch ? jsonMatch[1].trim() : content.trim(); + return JSON.parse(jsonStr) as T; + } catch (error) { + console.error("Failed to parse JSON response:", error); + // Try to parse without code blocks + try { + // Remove any leading/trailing non-JSON content + const cleanedContent = content + .replace(/^[^{]*/, "") + .replace(/[^}]*$/, ""); + return JSON.parse(cleanedContent) as T; + } catch { + console.error("Secondary parse also failed"); + return null; + } + } +} + +/** + * Research organization using AI model (fallback when Tavily is not available) + * Uses the configured AI model to generate organization profile based on knowledge + */ +async function researchOrganizationWithAI( + name: string, + domain?: string, + context?: string, + modelName?: string, + apiKeys?: ApiKeys, +): Promise<Partial<OrganizationProfile>> { + console.error( + "\n🤖 Using AI model fallback for organization research (Tavily not available)", + ); + + const now = new Date().toISOString(); + const model = getModel(modelName, apiKeys, "discover_organization"); + + // Check if it's a known enterprise company + const isEnterprise = isKnownEnterpriseCompany(name); + const knownDomain = domain || getKnownDomain(name); + + const prompt = `You are an expert business analyst. Research and provide information about the organization "${name}"${knownDomain ? ` (domain: ${knownDomain})` : ""}${context ? `. Additional context: ${context}` : ""}. + +Based on your knowledge, provide a comprehensive organization profile for EU AI Act compliance assessment. + +${isEnterprise ? `Note: ${name} is a known large enterprise company. Classify as "Enterprise" size.` : ""} + +RESPOND ONLY WITH VALID JSON in this exact format: +{ + "sector": "<Technology|Healthcare|Financial Services|Retail|Manufacturing|Education|Media & Entertainment|Energy|Telecommunications|Automotive|Other>", + "size": "<${isEnterprise ? "Enterprise" : "Startup|SME|Enterprise"}>", + "headquartersCountry": "<country name>", + "headquartersCity": "<city name>", + "euPresence": <true|false>, + "jurisdictions": ["<jurisdiction1>", "<jurisdiction2>"], + "aiMaturityLevel": "<Nascent|Developing|Mature|Leader>", + "certifications": ["<cert1>", "<cert2>"], + "hasQualityManagementSystem": <true|false>, + "hasRiskManagementSystem": <true|false>, + "description": "<brief description of the organization and its AI activities>" +} + +Focus on providing accurate information based on your knowledge. If uncertain, make reasonable inferences based on the company name, industry, and any context provided.`; + + try { + const result = await generateText({ + model, + prompt, + temperature: 0.1, + providerOptions: { + anthropic: { + thinking: { type: "disabled" }, + }, + openai: { + reasoningEffort: "low", + }, + google: { + thinkingConfig: { + thinkingLevel: "low", + includeThoughts: true, + }, + }, + }, + }); + + const responseText = result.text; + console.error( + `[AI Fallback] Generated response (${responseText.length} chars)`, + ); + + const parsedData = parseJSONResponse<{ + sector: string; + size: string; + headquartersCountry: string; + headquartersCity: string; + euPresence: boolean; + jurisdictions: string[]; + aiMaturityLevel: string; + certifications: string[]; + hasQualityManagementSystem: boolean; + hasRiskManagementSystem: boolean; + description: string; + }>(responseText); + + if (!parsedData) { + console.error("❌ Failed to parse AI response, using basic fallback"); + return createBasicFallbackProfile(name, domain); + } + + console.error(`✅ AI model provided organization profile for ${name}`); + console.error(` Sector: ${parsedData.sector}`); + console.error(` Size: ${parsedData.size}`); + console.error( + ` HQ: ${parsedData.headquartersCity}, ${parsedData.headquartersCountry}`, + ); + console.error(` EU Presence: ${parsedData.euPresence}`); + + return { + organization: { + name, + sector: parsedData.sector || "Technology", + size: (parsedData.size as "Startup" | "SME" | "Enterprise") || "SME", + jurisdiction: parsedData.jurisdictions || ["Unknown"], + euPresence: parsedData.euPresence ?? false, + headquarters: { + country: parsedData.headquartersCountry || "Unknown", + city: parsedData.headquartersCity || "Unknown", + }, + contact: { + email: knownDomain ? `contact@${knownDomain}` : "unknown@example.com", + website: knownDomain ? `https://${knownDomain}` : undefined, + }, + aiMaturityLevel: + (parsedData.aiMaturityLevel as + | "Nascent" + | "Developing" + | "Mature" + | "Leader") || "Developing", + aiSystemsCount: 0, + primaryRole: "Provider", + }, + regulatoryContext: { + applicableFrameworks: ["EU AI Act", "GDPR"], + complianceDeadlines: [ + { + date: "2025-02-02", + description: "Prohibited AI practices ban (Article 5)", + article: "Article 5", + }, + { + date: "2025-08-02", + description: "GPAI model obligations (Article 53)", + article: "Article 53", + }, + { + date: "2027-08-02", + description: "Full AI Act enforcement for high-risk systems", + article: "Article 113", + }, + ], + existingCertifications: parsedData.certifications || [], + hasQualityManagementSystem: + parsedData.hasQualityManagementSystem ?? false, + hasRiskManagementSystem: parsedData.hasRiskManagementSystem ?? false, + }, + metadata: { + createdAt: now, + lastUpdated: now, + completenessScore: 70, // AI-generated data is more complete than basic fallback + dataSource: "ai-model-research", + aiGeneratedProfile: { + description: parsedData.description, + modelUsed: modelName || "unknown", + }, + }, + }; + } catch (error) { + console.error("❌ AI model fallback failed:", error); + return createBasicFallbackProfile(name, domain); + } +} + +/** + * Basic fallback profile when both Tavily and AI model fail + */ +function createBasicFallbackProfile( + name: string, + domain?: string, +): Partial<OrganizationProfile> { + const now = new Date().toISOString(); + const isEnterprise = isKnownEnterpriseCompany(name); + const knownDomain = domain || getKnownDomain(name); + + return { + organization: { + name, + sector: "Technology", + size: isEnterprise ? "Enterprise" : "SME", + jurisdiction: ["Unknown"], + euPresence: false, + headquarters: { + country: "Unknown", + city: "Unknown", + }, + contact: { + email: knownDomain ? `contact@${knownDomain}` : "unknown@example.com", + website: knownDomain ? `https://${knownDomain}` : undefined, + }, + aiMaturityLevel: "Developing", + aiSystemsCount: 0, + primaryRole: "Provider", + }, + regulatoryContext: { + applicableFrameworks: ["EU AI Act", "GDPR"], + complianceDeadlines: [ + { + date: "2025-02-02", + description: "Prohibited AI practices ban (Article 5)", + article: "Article 5", + }, + { + date: "2025-08-02", + description: "GPAI model obligations (Article 53)", + article: "Article 53", + }, + { + date: "2027-08-02", + description: "Full AI Act enforcement for high-risk systems", + article: "Article 113", + }, + ], + existingCertifications: [], + hasQualityManagementSystem: false, + hasRiskManagementSystem: false, + }, + metadata: { + createdAt: now, + lastUpdated: now, + completenessScore: 30, + dataSource: "basic-fallback", + }, + }; +} + +/** + * Fallback profile when Tavily is not available + * @deprecated Use researchOrganizationWithAI instead + */ +function createFallbackProfile( + name: string, + domain?: string, +): Partial<OrganizationProfile> { + // Redirect to basic fallback - this is kept for backwards compatibility + return createBasicFallbackProfile(name, domain); +} + +/** + * Enrich organization profile with AI Act specific context + */ +function enrichWithAIActContext( + profile: Partial<OrganizationProfile>, +): OrganizationProfile { + // Ensure all required fields are present + const enriched: OrganizationProfile = { + organization: { + name: profile.organization?.name || "", + registrationNumber: profile.organization?.registrationNumber, + sector: profile.organization?.sector || "Unknown", + size: profile.organization?.size || "SME", + jurisdiction: profile.organization?.jurisdiction || ["EU"], + euPresence: profile.organization?.euPresence ?? true, + headquarters: profile.organization?.headquarters || { + country: "Unknown", + city: "Unknown", + }, + contact: profile.organization?.contact || { + email: "unknown@example.com", + }, + branding: profile.organization?.branding, + aiMaturityLevel: profile.organization?.aiMaturityLevel || "Nascent", + aiSystemsCount: profile.organization?.aiSystemsCount || 0, + primaryRole: profile.organization?.primaryRole || "Provider", + }, + regulatoryContext: { + applicableFrameworks: profile.regulatoryContext?.applicableFrameworks || [ + "EU AI Act", + ], + complianceDeadlines: profile.regulatoryContext?.complianceDeadlines || [], + existingCertifications: + profile.regulatoryContext?.existingCertifications || [], + hasAuthorizedRepresentative: + profile.regulatoryContext?.hasAuthorizedRepresentative, + notifiedBodyId: profile.regulatoryContext?.notifiedBodyId, + hasQualityManagementSystem: + profile.regulatoryContext?.hasQualityManagementSystem ?? false, + hasRiskManagementSystem: + profile.regulatoryContext?.hasRiskManagementSystem ?? false, + }, + metadata: { + createdAt: profile.metadata?.createdAt || new Date().toISOString(), + lastUpdated: profile.metadata?.lastUpdated || new Date().toISOString(), + completenessScore: profile.metadata?.completenessScore || 50, + dataSource: profile.metadata?.dataSource || "manual", + }, + }; + + // Add recommendations based on organization profile + if (!enriched.organization.euPresence) { + // Non-EU organizations need authorized representative + enriched.regulatoryContext.hasAuthorizedRepresentative = + enriched.regulatoryContext.hasAuthorizedRepresentative ?? false; + } + + return enriched; +} + +/** + * Main organization discovery function + */ +export async function discoverOrganization( + input: DiscoverOrganizationInput & { + model?: string; + apiKeys?: ApiKeys; + tavilyApiKey?: string; + }, +): Promise<OrganizationProfile> { + const { organizationName, domain, context, model, apiKeys, tavilyApiKey } = + input; + + // Step 1: Research organization details + // Use passed parameters from Gradio UI - fall back to AI model if Tavily not available + let researchedData: Partial<OrganizationProfile>; + + if (!tavilyApiKey) { + // No Tavily API key - use AI model fallback directly + if (!model || !apiKeys) { + throw new Error( + "Either Tavily API key or AI model configuration is required. Please provide API keys in the Model Settings panel.", + ); + } + console.warn( + "⚠️ Tavily API key not provided, using AI model for organization research", + ); + researchedData = await researchOrganizationWithAI( + organizationName, + domain, + context, + model, + apiKeys, + ); + } else { + // Tavily API key provided - try Tavily first, fall back to AI model on error + try { + researchedData = await researchOrganization( + organizationName, + domain, + context, + tavilyApiKey, + model, + apiKeys, + ); + } catch (error) { + // If Tavily fails and we have model/API keys, try AI fallback + if (model && apiKeys) { + console.warn("⚠️ Tavily research failed, using AI model fallback"); + researchedData = await researchOrganizationWithAI( + organizationName, + domain, + context, + model, + apiKeys, + ); + } else { + throw error; + } + } + } + + console.error("Raw result:", JSON.stringify(researchedData, null, 2)); + + // Step 2: Enrich with AI Act context + const enrichedProfile = enrichWithAIActContext(researchedData); + + // Step 3: Return complete profile + return enrichedProfile; +} diff --git a/packages/eu-ai-act-mcp/src/types/colorthief.d.ts b/packages/eu-ai-act-mcp/src/types/colorthief.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..4e0778a992d6d913e2fa64999ab724c77328ddc8 --- /dev/null +++ b/packages/eu-ai-act-mcp/src/types/colorthief.d.ts @@ -0,0 +1,31 @@ +/** + * Type declarations for colorthief package + * @see https://github.com/lokesh/color-thief + */ + +declare module "colorthief" { + type RGBColor = [number, number, number]; + + interface ColorThief { + /** + * Get the dominant color from an image URL + * @param sourceImage - URL to the image + * @param quality - Quality setting (1 is highest, 10 is default) + * @returns RGB color array + */ + getColor(sourceImage: string, quality?: number): Promise<RGBColor>; + + /** + * Get a color palette from an image URL + * @param sourceImage - URL to the image + * @param colorCount - Number of colors to return (default 10) + * @param quality - Quality setting (1 is highest, 10 is default) + * @returns Array of RGB color arrays + */ + getPalette(sourceImage: string, colorCount?: number, quality?: number): Promise<RGBColor[]>; + } + + const ColorThief: ColorThief; + export default ColorThief; +} + diff --git a/packages/eu-ai-act-mcp/src/types/index.ts b/packages/eu-ai-act-mcp/src/types/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..039b2e22e1ba35bdb32a08bbd08c2c0878549dea --- /dev/null +++ b/packages/eu-ai-act-mcp/src/types/index.ts @@ -0,0 +1,677 @@ +/** + * EU AI Act Compliance Types + * Based on Regulation (EU) 2024/1689 - AI Act + */ + +/** + * Organization Size Classification + * Relevant for determining compliance obligations and support measures + */ +export type OrganizationSize = + | "Startup" // Early stage company + | "SME" // Small and Medium Enterprises (< 250 employees) + | "Enterprise" // Large enterprise (> 250 employees) + | "Public Body" // Government or public sector entity + | "Micro Enterprise"; // < 10 employees + +/** + * AI Maturity Level + * Indicates organization's AI adoption stage + */ +export type AIMaturityLevel = + | "Nascent" // Just starting AI adoption + | "Developing" // Actively implementing AI + | "Mature" // Mature AI operations + | "Leader"; // Leading AI practice + +/** + * Risk Category per EU AI Act Article 6 and Annex III + */ +export type RiskCategory = + | "Unacceptable" // Prohibited AI practices (Article 5) + | "High" // High-risk AI systems (Annex III) + | "Limited" // Limited risk requiring transparency (Article 50) + | "Minimal"; // Minimal or no risk + +/** + * Deployment Model + */ +export type DeploymentModel = + | "On-premise" + | "Cloud" + | "Cloud (API-based)" + | "Hybrid" + | "Edge" + | "SaaS"; + +/** + * Provider Role per Article 3(3) + */ +export type ProviderRole = + | "Provider" // Develops/places AI on market + | "Provider/Deployer" // Both provider and deployer role + | "Deployer" // Uses AI under own authority + | "Importer" // Places on EU market from non-EU + | "Distributor" // Makes available on market + | "Authorized Representative"; // Represents non-EU provider + +/** + * Conformity Assessment Type per Article 43 + */ +export type ConformityAssessmentType = + | "Internal Control" // Article 43(1) - Annex VI + | "Internal Control (Articles 43, 46)" // Derogation case + | "Third Party Assessment" // Article 43(2) - Annex VII + | "Not Required" // For non-high-risk systems + | "Not Required - Transparency Obligations Only" // Limited risk systems + | "Pending"; + +/** + * Organization Profile Schema + * Based on Article 16 (Provider Obligations) and Article 49 (Registration) + * References Annex VIII for registration requirements + */ +export interface OrganizationProfile { + /** Basic Organization Information */ + organization: { + /** Legal name of the organization */ + name: string; + + /** Registration/VAT number if applicable */ + registrationNumber?: string; + + /** Primary business sector (e.g., Healthcare, Finance, Manufacturing) */ + sector: string; + + /** Organization size classification */ + size: OrganizationSize; + + /** Countries of operation */ + jurisdiction: string[]; + + /** EU Member State presence */ + euPresence: boolean; + + /** Headquarters location */ + headquarters: { + country: string; + city: string; + address?: string; + }; + + /** Contact information per Article 16(f) */ + contact: { + email: string; + phone?: string; + website?: string; + }; + + /** Branding information extracted from organization's visual identity */ + branding?: { + /** URL to the organization's logo */ + logoUrl?: string; + /** Primary brand color in hex format (e.g., "#1DA1F2") */ + primaryColor?: string; + /** Color palette extracted from logo (array of hex colors) */ + palette?: string[]; + /** Source of the branding data */ + source?: "logo-extraction" | "known-brand" | "fallback"; + }; + + /** AI maturity assessment */ + aiMaturityLevel: AIMaturityLevel; + + /** Number of AI systems currently deployed */ + aiSystemsCount?: number; + + /** Primary role in AI value chain */ + primaryRole: ProviderRole; + }; + + /** Regulatory Context per Article 16 & 49 */ + regulatoryContext: { + /** Applicable regulatory frameworks beyond AI Act */ + applicableFrameworks: string[]; + + /** Key compliance deadlines from AI Act timeline */ + complianceDeadlines: Array<{ + date: string; // ISO 8601 date + description: string; + article: string; // Reference to AI Act article + }>; + + /** Existing certifications (ISO, SOC2, etc.) */ + existingCertifications: string[]; + + /** Has appointed authorized representative (if non-EU) per Article 22 */ + hasAuthorizedRepresentative?: boolean; + + /** Notified body information if applicable */ + notifiedBodyId?: string; + + /** Quality management system status per Article 17 */ + hasQualityManagementSystem: boolean; + + /** Risk management system status per Article 9 */ + hasRiskManagementSystem: boolean; + }; + + /** Metadata */ + metadata: { + /** When this profile was created */ + createdAt: string; + + /** Last update timestamp */ + lastUpdated: string; + + /** Profile completeness score (0-100) */ + completenessScore: number; + + /** Data source (manual entry, API discovery, tavily-research, ai-model-research, etc.) */ + dataSource: string; + + /** Tavily research results (when using Tavily for discovery) */ + tavilyResults?: { + overview: string; + aiCapabilities: string; + compliance: string; + sources: string[]; + }; + + /** AI-generated profile data (when using AI model fallback for discovery) */ + aiGeneratedProfile?: { + description?: string; + modelUsed: string; + }; + }; +} + +/** + * AI System Profile Schema + * Based on Article 11 (Technical Documentation), Annex III (High-Risk Classification), + * Annex IV (Technical Documentation Requirements), and Annex VIII (Registration Requirements) + */ +export interface AISystemProfile { + /** Basic System Information */ + system: { + /** System name/identifier */ + name: string; + + /** Unique system ID (for registration per Article 49) */ + systemId?: string; + + /** Detailed description of intended purpose per Annex IV(1)(a) */ + description: string; + + /** Intended purpose per Article 3(12) */ + intendedPurpose: string; + + /** Version/release information */ + version: string; + + /** Development status */ + status: "Development" | "Testing" | "Production" | "Deprecated"; + + /** System owner/provider information */ + provider: { + name: string; + role: ProviderRole; + contact: string; + }; + }; + + /** Risk Classification per Article 6 & Annex III */ + riskClassification: { + /** Primary risk category */ + category: RiskCategory; + + /** If high-risk, specific Annex III classification */ + annexIIICategory?: string; + + /** Justification for classification per Article 6(3) */ + justification: string; + + /** Whether system falls under safety legislation (Article 6(1)) */ + safetyComponent: boolean; + + /** Risk level score (0-100) */ + riskScore: number; + + /** Requires conformity assessment per Article 43 */ + conformityAssessmentRequired: boolean; + + /** Type of conformity assessment */ + conformityAssessmentType: ConformityAssessmentType; + + /** Regulatory references and applicable articles */ + regulatoryReferences?: string[]; + }; + + /** Technical Characteristics per Annex IV */ + technicalDetails: { + /** AI technology/approach (ML, DL, Expert System, etc.) */ + aiTechnology: string[]; + + /** Types of data processed per Article 10 */ + dataProcessed: string[]; + + /** Sensitive data handling (biometric, health, etc.) */ + processesSpecialCategoryData: boolean; + + /** Deployment architecture */ + deploymentModel: DeploymentModel; + + /** Third-party vendor/provider if applicable */ + vendor?: string; + + /** Training data information per Article 10 */ + trainingData?: { + description: string; + sources: string[]; + volume?: string; + biasAssessment: boolean; + biasDetected?: string[]; + }; + + /** Accuracy metrics per Article 15 */ + accuracy?: { + precision?: number; + recall?: number; + f1Score?: number; + performancePerDemographic?: { + [demographic: string]: number; + }; + }; + + /** Model details for GPAI systems */ + modelDetails?: { + baseModel: string; + finetuned: boolean; + trainingApproach: string; + contextWindowSize?: number; + }; + + /** Integration points with other systems */ + integrations: string[]; + + /** Human oversight measures per Article 14 */ + humanOversight: { + enabled: boolean; + description?: string; + level?: string; // e.g., "Low", "Medium", "High" + documentation?: string; + escalationThreshold?: string; + }; + }; + + /** Compliance Status per Article 16 */ + complianceStatus: { + /** Technical documentation status per Article 11 */ + hasTechnicalDocumentation: boolean; + + /** Conformity assessment status per Article 43 */ + conformityAssessmentStatus: "Not Started" | "In Progress" | "Completed" | "Not Required"; + + /** EU Declaration of Conformity per Article 47 */ + hasEUDeclaration: boolean; + + /** CE marking affixed per Article 48 */ + hasCEMarking: boolean; + + /** Registered in EU database per Article 49 */ + registeredInEUDatabase: boolean; + + /** Post-market monitoring per Article 72 */ + hasPostMarketMonitoring: boolean; + + /** Logging capabilities per Article 12 */ + hasAutomatedLogging: boolean; + + /** Quality management system per Article 17 */ + qualityManagementSystem?: boolean; + + /** Risk management system per Article 9 */ + riskManagementSystem?: boolean; + + /** Transparency implementation for Article 50 systems */ + transparencyImplemented?: boolean; + + /** Compliance deadline */ + complianceDeadline?: string; + + /** Estimated effort to achieve compliance */ + estimatedComplianceEffort?: string; + + /** Last compliance assessment date */ + lastAssessmentDate?: string; + + /** Identified compliance gaps */ + identifiedGaps: string[]; + }; + + /** Metadata */ + metadata: { + /** When this profile was created */ + createdAt: string; + + /** Last update timestamp */ + lastUpdated: string; + + /** Data source */ + dataSource: string; + + /** Discovery method (automated scan, manual entry, API, ai-model-fallback, etc.) */ + discoveryMethod: string; + + /** Research sources (Tavily, documentation references) */ + researchSources?: string[]; + + /** AI-generated profile data (when using AI model fallback for discovery) */ + aiGeneratedProfile?: { + modelUsed: string; + }; + }; +} + +/** + * Discovery Response for AI Systems + * Aggregate response for multiple systems with summary + */ +export interface AISystemsDiscoveryResponse { + /** List of discovered AI systems */ + systems: AISystemProfile[]; + + /** Risk summary across all systems */ + riskSummary: { + unacceptableRiskCount: number; + highRiskCount: number; + limitedRiskCount: number; + minimalRiskCount: number; + totalCount: number; + }; + + /** Compliance summary */ + complianceSummary: { + fullyCompliantCount: number; + partiallyCompliantCount: number; + nonCompliantCount: number; + requiresAttention: AISystemProfile[]; + criticalGapCount?: number; + highGapCount?: number; + overallCompliancePercentage?: number; + }; + + /** Regulatory framework information */ + regulatoryFramework?: { + legislation: string; + officialJournal: string; + entryIntoForce: string; + implementationTimeline: string; + jurisdiction: string; + }; + + /** Compliance deadlines */ + complianceDeadlines?: { + highRisk: string; + limitedRisk: string; + generalGPAI: string; + }; + + /** Discovery sources */ + discoverySources?: string[]; + + /** Discovery metadata */ + discoveryMetadata: { + timestamp: string; + method: string; + coverage: string; // Estimated coverage of discovery + researchIntegration?: string; + conformityAssessmentUrgency?: string; + }; +} + +/** + * Tool Input Schemas + */ +export interface DiscoverOrganizationInput { + /** Organization name or identifier */ + organizationName: string; + + /** Optional domain/website for research */ + domain?: string; + + /** Additional context for discovery */ + context?: string; +} + +export interface DiscoverAIServicesInput { + /** Organization context (from discover_organization or manual) */ + organizationContext?: OrganizationProfile; + + /** Specific system names to discover */ + systemNames?: string[]; + + /** Scope of discovery (e.g., "all", "high-risk-only") */ + scope?: string; + + /** Additional context about the AI systems to discover */ + context?: string; +} + +/** + * Compliance Assessment Types + * Based on EU AI Act requirements for gap analysis and remediation + */ + +/** + * Gap Severity Level + */ +export type GapSeverity = "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"; + +/** + * Remediation Effort Level + */ +export type RemediationEffort = "LOW" | "MEDIUM" | "HIGH"; + +/** + * Gap Analysis Item + * Represents a single compliance gap identified during assessment + */ +export interface GapAnalysis { + /** Unique identifier for the gap */ + id: string; + + /** Severity level of the gap */ + severity: GapSeverity; + + /** Category of the gap (e.g., "Technical Documentation", "Risk Management") */ + category: string; + + /** Detailed description of the gap */ + description: string; + + /** AI systems affected by this gap */ + affectedSystems: string[]; + + /** EU AI Act article reference */ + articleReference: string; + + /** Current state description */ + currentState: string; + + /** Required state per regulation */ + requiredState: string; + + /** Estimated effort to remediate */ + remediationEffort: RemediationEffort; + + /** Estimated cost range */ + estimatedCost?: string; + + /** Compliance deadline */ + deadline?: string; +} + +/** + * Remediation Recommendation + */ +export interface Recommendation { + /** Unique identifier */ + id: string; + + /** Priority (1-10, 1 being highest) */ + priority: number; + + /** Recommendation title */ + title: string; + + /** Detailed description */ + description: string; + + /** EU AI Act article reference */ + articleReference: string; + + /** Step-by-step implementation guide */ + implementationSteps: string[]; + + /** Estimated effort */ + estimatedEffort: string; + + /** Expected outcome */ + expectedOutcome: string; + + /** Dependencies on other recommendations */ + dependencies?: string[]; +} + +/** + * Compliance Documentation Templates + * Markdown templates for various EU AI Act documentation requirements + * + * ⚠️ CURRENT IMPLEMENTATION NOTE: + * Only 2 templates are currently generated for API cost and speed optimization: + * - ✅ riskManagementTemplate (Article 9) + * - ✅ technicalDocumentation (Article 11 / Annex IV) + * + * The remaining 6 templates are planned but NOT YET IMPLEMENTED: + * - 🔜 conformityAssessment (Article 43) + * - 🔜 transparencyNotice (Article 50) + * - 🔜 qualityManagementSystem (Article 17) + * - 🔜 humanOversightProcedure (Article 14) + * - 🔜 dataGovernancePolicy (Article 10) + * - 🔜 incidentReportingProcedure (Article 62) + */ +export interface ComplianceDocumentation { + /** ✅ IMPLEMENTED: Risk Management System template (Article 9) */ + riskManagementTemplate?: string; + + /** ✅ IMPLEMENTED: Technical Documentation template (Article 11, Annex IV) */ + technicalDocumentation?: string; + + // ----- PLANNED BUT NOT YET IMPLEMENTED (for speed & cost optimization) ----- + + // /** 🔜 PLANNED: Conformity Assessment template (Article 43) */ + // conformityAssessment?: string; + + // /** 🔜 PLANNED: Transparency Notice template (Article 50) */ + // transparencyNotice?: string; + + // /** 🔜 PLANNED: Quality Management System template (Article 17) */ + // qualityManagementSystem?: string; + + // /** 🔜 PLANNED: Human Oversight Procedure template (Article 14) */ + // humanOversightProcedure?: string; + + // /** 🔜 PLANNED: Data Governance Policy template (Article 10) */ + // dataGovernancePolicy?: string; + + // /** 🔜 PLANNED: Incident Reporting Procedure template (Article 62) */ + // incidentReportingProcedure?: string; +} + +/** + * Supported AI Models for Compliance Assessment + */ +export type AIModelType = + | "claude-4.5" // Anthropic Claude Sonnet 4.5 (default) + | "claude-opus" // Anthropic Claude Opus 4 + | "gpt-5" // OpenAI GPT-5 + | "grok-4-1" // xAI Grok 4.1 Fast Reasoning + | "gemini-3"; // Google Gemini 3 Pro + +/** + * Compliance Assessment Input + */ +export interface ComplianceAssessmentInput { + /** Organization context from discover_organization tool */ + organizationContext?: OrganizationProfile; + + /** AI services context from discover_ai_services tool */ + aiServicesContext?: AISystemsDiscoveryResponse; + + /** Specific focus areas for assessment */ + focusAreas?: string[]; + + /** Whether to generate documentation templates (default: true) */ + generateDocumentation?: boolean; + + /** AI model to use for assessment (passed from UI settings) */ + model?: AIModelType; +} + +/** + * Compliance Assessment Response + */ +export interface ComplianceAssessmentResponse { + /** Assessment results */ + assessment: { + /** Overall compliance score (0-100) */ + overallScore: number; + + /** Overall risk level */ + riskLevel: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW"; + + /** Identified compliance gaps */ + gaps: GapAnalysis[]; + + /** Remediation recommendations */ + recommendations: Recommendation[]; + + /** Compliance status by EU AI Act article */ + complianceByArticle: Record<string, { + compliant: boolean; + gaps: string[]; + }>; + }; + + /** Generated documentation templates */ + documentation?: ComplianceDocumentation; + + /** Chain-of-thought reasoning explanation */ + reasoning: string; + + /** Assessment metadata */ + metadata: { + /** Assessment timestamp */ + assessmentDate: string; + + /** Assessment version */ + assessmentVersion: string; + + /** AI model used for assessment */ + modelUsed: string; + + /** Organization assessed */ + organizationAssessed?: string; + + /** Systems assessed */ + systemsAssessed: string[]; + + /** Focus areas */ + focusAreas: string[]; + + /** Paths to generated markdown documentation files */ + documentationFiles?: string[]; + }; +} + diff --git a/packages/eu-ai-act-mcp/src/utils/branding.ts b/packages/eu-ai-act-mcp/src/utils/branding.ts new file mode 100644 index 0000000000000000000000000000000000000000..3cec9f97058bd467f29b09a944e6a03dc8042bc1 --- /dev/null +++ b/packages/eu-ai-act-mcp/src/utils/branding.ts @@ -0,0 +1,171 @@ +/** + * Branding utilities for organization discovery + * Handles logo discovery and color extraction from brand assets + */ + +import ColorThief from "colorthief"; +import type { OrganizationProfile } from "../types/index.js"; +import { KNOWN_BRANDS } from "./brands.js"; + +/** + * Convert RGB array to hex color string + */ +export function rgbToHex(rgb: [number, number, number]): string { + return `#${rgb.map(x => x.toString(16).padStart(2, '0')).join('')}`.toUpperCase(); +} + +/** + * Convert HSL to Hex color + */ +export function hslToHex(h: number, s: number, l: number): string { + s /= 100; + l /= 100; + const a = s * Math.min(l, 1 - l); + const f = (n: number) => { + const k = (n + h / 30) % 12; + const color = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return Math.round(255 * color).toString(16).padStart(2, '0'); + }; + return `#${f(0)}${f(8)}${f(4)}`.toUpperCase(); +} + +/** + * Extract brand colors from a logo URL using ColorThief + */ +export async function extractColorsFromLogo( + logoUrl: string +): Promise<{ primaryColor: string; palette: string[] } | null> { + try { + console.error(`🎨 Extracting colors from logo: ${logoUrl}`); + + // ColorThief.getColor and getPalette work with image URLs in Node.js + const dominantColor = await ColorThief.getColor(logoUrl); + const colorPalette = await ColorThief.getPalette(logoUrl, 5); + + const primaryColor = rgbToHex(dominantColor as [number, number, number]); + const palette = colorPalette.map((rgb: number[]) => + rgbToHex(rgb as [number, number, number]) + ); + + console.error(`✅ Extracted primary color: ${primaryColor}`); + console.error(`✅ Extracted palette: ${palette.join(", ")}`); + + return { primaryColor, palette }; + } catch (error) { + console.error(`❌ Failed to extract colors from logo: ${error}`); + return null; + } +} + +/** + * Find logo URL from search results + */ +export function findLogoFromSearchResults( + searchResults: any, + domain?: string +): string | undefined { + const results = searchResults.results || []; + + // Look for logo URLs in search results + for (const result of results) { + const url = result.url?.toLowerCase() || ""; + + // Check if this is an image or logo page + if (url.includes("logo") || url.includes("brand") || url.includes("press-kit")) { + // Try to find image URL in content + const imgMatch = result.content?.match( + /https?:\/\/[^\s"']+\.(png|jpg|jpeg|svg|webp)/i + ); + if (imgMatch) { + console.error(`🖼️ Found logo URL from press/brand page: ${imgMatch[0]}`); + return imgMatch[0]; + } + } + } + + // Use Clearbit as fallback for logo URL if domain is available + if (domain) { + return `https://logo.clearbit.com/${domain}`; + } + + return undefined; +} + +/** + * Get branding information for an organization + */ +export async function getBrandingInfo( + name: string, + domain?: string, + searchResults?: any +): Promise<OrganizationProfile["organization"]["branding"]> { + const normalizedName = name.toLowerCase().replace(/[^a-z0-9]/g, ""); + + // Check if this is a known brand + for (const [brandKey, brandData] of Object.entries(KNOWN_BRANDS)) { + if (normalizedName.includes(brandKey) || brandKey.includes(normalizedName)) { + console.error(`✅ Found known brand: ${brandKey}`); + return { + logoUrl: brandData.logoUrl, + primaryColor: brandData.primaryColor, + palette: brandData.palette, + source: "known-brand", + }; + } + } + + // Try to find logo from search results + let logoUrl: string | undefined; + if (searchResults) { + logoUrl = findLogoFromSearchResults(searchResults, domain); + } + + // Use Clearbit as fallback for logo URL + if (!logoUrl && domain) { + logoUrl = `https://logo.clearbit.com/${domain}`; + console.error(`🔄 Using Clearbit logo API: ${logoUrl}`); + } + + // If we have a logo URL, try to extract colors + if (logoUrl) { + try { + const colors = await extractColorsFromLogo(logoUrl); + if (colors) { + return { + logoUrl, + primaryColor: colors.primaryColor, + palette: colors.palette, + source: "logo-extraction", + }; + } + } catch (error) { + console.error(`⚠️ Color extraction failed, using fallback: ${error}`); + } + } + + // Fallback - generate colors based on company name hash + const hash = normalizedName.split("").reduce((acc, char) => { + return char.charCodeAt(0) + ((acc << 5) - acc); + }, 0); + + const hue = Math.abs(hash) % 360; + const primaryColor = hslToHex(hue, 70, 50); + const palette = [ + primaryColor, + hslToHex(hue, 60, 40), + hslToHex(hue, 80, 60), + hslToHex((hue + 30) % 360, 70, 50), + hslToHex((hue + 180) % 360, 70, 50), + ]; + + console.error(`🎨 Generated fallback colors based on name hash`); + console.error(`Primary: ${primaryColor}, Palette: ${palette.join(", ")}`); + + return { + logoUrl, + primaryColor, + palette, + source: "fallback", + }; +} + diff --git a/packages/eu-ai-act-mcp/src/utils/brands.ts b/packages/eu-ai-act-mcp/src/utils/brands.ts new file mode 100644 index 0000000000000000000000000000000000000000..7fd64c5bc5873107cfa554dced1c101d1f90cd92 --- /dev/null +++ b/packages/eu-ai-act-mcp/src/utils/brands.ts @@ -0,0 +1,144 @@ +/** + * Known brand colors and logos for major companies + * Fallback when logo extraction fails + */ + +export interface BrandData { + logoUrl: string; + primaryColor: string; + palette: string[]; +} + +export const KNOWN_BRANDS: Record<string, BrandData> = { + // Tech Giants + google: { + logoUrl: "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", + primaryColor: "#4285F4", + palette: ["#4285F4", "#EA4335", "#FBBC05", "#34A853"], + }, + microsoft: { + logoUrl: "https://img-prod-cms-rt-microsoft-com.akamaized.net/cms/api/am/imageFileData/RE1Mu3b", + primaryColor: "#00A4EF", + palette: ["#F25022", "#7FBA00", "#00A4EF", "#FFB900"], + }, + apple: { + logoUrl: "https://www.apple.com/ac/globalnav/7/en_US/images/be15095f-5a20-57d0-ad14-cf4c638e223a/globalnav_apple_image__b5er5ngrzxqq_large.svg", + primaryColor: "#000000", + palette: ["#000000", "#555555", "#A3AAAE"], + }, + amazon: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/a/a9/Amazon_logo.svg", + primaryColor: "#FF9900", + palette: ["#FF9900", "#232F3E", "#146EB4"], + }, + meta: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/7/7b/Meta_Platforms_Inc._logo.svg", + primaryColor: "#0081FB", + palette: ["#0081FB", "#0064E0", "#0A66C2"], + }, + facebook: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/5/51/Facebook_f_logo_%282019%29.svg", + primaryColor: "#1877F2", + palette: ["#1877F2", "#166FE5", "#0866FF"], + }, + // AI Companies + openai: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/4/4d/OpenAI_Logo.svg", + primaryColor: "#10A37F", + palette: ["#10A37F", "#000000", "#FFFFFF"], + }, + anthropic: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg", + primaryColor: "#D4A574", + palette: ["#D4A574", "#1A1A1A", "#F5F5F5"], + }, + // Enterprise Tech + salesforce: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/f/f9/Salesforce.com_logo.svg", + primaryColor: "#00A1E0", + palette: ["#00A1E0", "#032D60", "#1B96FF"], + }, + oracle: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/5/50/Oracle_logo.svg", + primaryColor: "#F80000", + palette: ["#F80000", "#000000", "#312D2A"], + }, + sap: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/5/59/SAP_2011_logo.svg", + primaryColor: "#0FAAFF", + palette: ["#0FAAFF", "#000000", "#354A5F"], + }, + ibm: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/5/51/IBM_logo.svg", + primaryColor: "#0530AD", + palette: ["#0530AD", "#000000", "#161616"], + }, + // European Tech + spotify: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/1/19/Spotify_logo_without_text.svg", + primaryColor: "#1DB954", + palette: ["#1DB954", "#191414", "#FFFFFF"], + }, + // Cloud Providers + aws: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/9/93/Amazon_Web_Services_Logo.svg", + primaryColor: "#FF9900", + palette: ["#FF9900", "#232F3E", "#146EB4"], + }, + // Social/Tech + twitter: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/6/6f/Logo_of_Twitter.svg", + primaryColor: "#1DA1F2", + palette: ["#1DA1F2", "#14171A", "#657786"], + }, + x: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/c/ce/X_logo_2023.svg", + primaryColor: "#000000", + palette: ["#000000", "#FFFFFF"], + }, + linkedin: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/c/ca/LinkedIn_logo_initials.png", + primaryColor: "#0A66C2", + palette: ["#0A66C2", "#000000", "#FFFFFF"], + }, + // Healthcare/Pharma + pfizer: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/e/e3/Pfizer_%282021%29.svg", + primaryColor: "#0093D0", + palette: ["#0093D0", "#003087", "#FFFFFF"], + }, + // Automotive + tesla: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/e/e8/Tesla_logo.png", + primaryColor: "#E82127", + palette: ["#E82127", "#000000", "#FFFFFF"], + }, + // Finance + visa: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/5/5e/Visa_Inc._logo.svg", + primaryColor: "#1A1F71", + palette: ["#1A1F71", "#F7B600", "#FFFFFF"], + }, + mastercard: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/2/2a/Mastercard-logo.svg", + primaryColor: "#EB001B", + palette: ["#EB001B", "#FF5F00", "#F79E1B"], + }, + stripe: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/b/ba/Stripe_Logo%2C_revised_2016.svg", + primaryColor: "#635BFF", + palette: ["#635BFF", "#00D4FF", "#0A2540"], + }, + // Consulting + accenture: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/c/cd/Accenture.svg", + primaryColor: "#A100FF", + palette: ["#A100FF", "#000000", "#FFFFFF"], + }, + deloitte: { + logoUrl: "https://upload.wikimedia.org/wikipedia/commons/5/56/Deloitte.svg", + primaryColor: "#86BC25", + palette: ["#86BC25", "#000000", "#FFFFFF"], + }, +}; + diff --git a/packages/eu-ai-act-mcp/src/utils/domain.ts b/packages/eu-ai-act-mcp/src/utils/domain.ts new file mode 100644 index 0000000000000000000000000000000000000000..03ca59f5345f19c6fb6a5612842f677882e34614 --- /dev/null +++ b/packages/eu-ai-act-mcp/src/utils/domain.ts @@ -0,0 +1,72 @@ +/** + * Domain utilities for organization discovery + * Handles domain extraction and discovery from search results + */ + +/** + * Extract domain from URL + */ +export function extractDomainFromUrl(url: string): string | undefined { + try { + const urlObj = new URL(url); + return urlObj.hostname; + } catch { + return undefined; + } +} + +/** + * Find company domain from search results + */ +export function findCompanyDomain( + searchResults: any, + companyName: string +): string | undefined { + const results = searchResults.results || []; + + // Look for official company website in results + for (const result of results) { + const url = result.url?.toLowerCase() || ""; + const title = result.title?.toLowerCase() || ""; + + // Check if this looks like an official company website + if ( + title.includes("official") || + title.includes("about") || + url.includes("/about") + ) { + const domain = extractDomainFromUrl(result.url); + if ( + domain && + !domain.includes("linkedin") && + !domain.includes("wiki") && + !domain.includes("news") + ) { + console.error(`🌐 Found official company domain: ${domain}`); + return domain; + } + } + } + + // If no official website found, try to find main company domain + for (const result of results) { + const url = result.url?.toLowerCase() || ""; + const domain = extractDomainFromUrl(result.url); + + // Skip knowledge bases, wikis, news sites + if ( + domain && + !domain.includes("linkedin") && + !domain.includes("wiki") && + !domain.includes("news") && + !domain.includes("ebsco") && + !domain.includes("globaldata") + ) { + console.error(`🌐 Found company domain from search: ${domain}`); + return domain; + } + } + + return undefined; +} + diff --git a/packages/eu-ai-act-mcp/src/utils/model.ts b/packages/eu-ai-act-mcp/src/utils/model.ts new file mode 100644 index 0000000000000000000000000000000000000000..51119985b840d247295ee2f82cd895abef44f4bf --- /dev/null +++ b/packages/eu-ai-act-mcp/src/utils/model.ts @@ -0,0 +1,147 @@ +/** + * Shared AI Model Utility + * + * Get the AI model based on model selection and API keys from Gradio UI. + * + * PRODUCTION: API keys MUST be provided by users via Gradio UI. + * No environment variable fallback! + * + * LOCAL DEV: Can use env vars for convenience (OPENAI_API_KEY, etc.) + * + * Supports: "gpt-oss" (Modal FREE), "gpt-5" (OpenAI), "grok-4-1" (xAI), + * "claude-4.5" (Anthropic), "claude-opus" (Anthropic), "gemini-3" (Google) + * + * Default: gpt-oss (FREE via Modal.com - no API key required!) + */ + +import { createXai } from "@ai-sdk/xai"; +import { createOpenAI } from "@ai-sdk/openai"; +import { createAnthropic } from "@ai-sdk/anthropic"; +import { createGoogleGenerativeAI } from "@ai-sdk/google"; + +// Default GPT-OSS endpoint (FREE model hosted on Modal.com) +const DEFAULT_GPT_OSS_ENDPOINT = "https://vasilis--gpt-oss-vllm-inference-serve.modal.run"; + +/** + * API Keys object - passed from Gradio UI via request headers + */ +export interface ApiKeys { + openaiApiKey?: string; + anthropicApiKey?: string; + googleApiKey?: string; + xaiApiKey?: string; + modalEndpointUrl?: string; +} + +/** + * Get API keys from either direct parameter or environment variables (LOCAL ONLY) + * + * In PRODUCTION: Only use apiKeys parameter (from Gradio UI) + * In LOCAL (development): Fall back to environment variables + */ +function resolveApiKeys(apiKeys?: ApiKeys): ApiKeys { + const isProduction = process.env.NODE_ENV === "production"; + + // In production, ONLY use API keys from Gradio UI (apiKeys parameter) + // In local dev, allow env var fallback for convenience + if (isProduction) { + return { + openaiApiKey: apiKeys?.openaiApiKey, + anthropicApiKey: apiKeys?.anthropicApiKey, + googleApiKey: apiKeys?.googleApiKey, + xaiApiKey: apiKeys?.xaiApiKey, + modalEndpointUrl: apiKeys?.modalEndpointUrl || DEFAULT_GPT_OSS_ENDPOINT, + }; + } + + // LOCAL development: allow env var fallback + return { + openaiApiKey: apiKeys?.openaiApiKey || process.env.OPENAI_API_KEY, + anthropicApiKey: apiKeys?.anthropicApiKey || process.env.ANTHROPIC_API_KEY, + googleApiKey: apiKeys?.googleApiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY, + xaiApiKey: apiKeys?.xaiApiKey || process.env.XAI_API_KEY, + modalEndpointUrl: apiKeys?.modalEndpointUrl || process.env.MODAL_ENDPOINT_URL || DEFAULT_GPT_OSS_ENDPOINT, + }; +} + +/** + * Get the AI model based on model selection and user-provided API keys + * + * @param modelName - Model to use (default: "gpt-oss", or read from AI_MODEL env) + * @param apiKeys - API keys provided by user via Gradio UI (optional, falls back to env vars) + * @param context - Optional context string for logging + * @returns The configured AI model instance + * + * Supported Models: + * - gpt-oss: OpenAI GPT-OSS 20B via Modal.com (FREE - no API key needed!) + * - claude-4.5: Anthropic Claude Sonnet 4.5 (requires anthropicApiKey) + * - claude-opus: Anthropic Claude Opus 4 (requires anthropicApiKey) + * - gpt-5: OpenAI GPT-5 (requires openaiApiKey) + * - grok-4-1: xAI Grok 4.1 Fast Reasoning (requires xaiApiKey) + * - gemini-3: Google Gemini 3 Pro (requires googleApiKey) + */ +export function getModel(modelName?: string, apiKeys?: ApiKeys, context?: string) { + // Model name: parameter > env var > default to gpt-oss + const model = modelName || process.env.AI_MODEL || "gpt-oss"; + // Resolve API keys: parameter > env vars + const keys = resolveApiKeys(apiKeys); + const logPrefix = context ? `[${context}]` : "[getModel]"; + + console.error(`${logPrefix} Using AI model: ${model}`); + + // GPT-OSS via Modal.com (FREE! No API key required!) + if (model === "gpt-oss") { + // Get endpoint and strip trailing slashes to avoid double-slash URLs + const modalEndpoint = (keys.modalEndpointUrl || DEFAULT_GPT_OSS_ENDPOINT).replace(/\/+$/, ""); + // Create OpenAI-compatible client pointing to Modal endpoint + // Pass a dummy apiKey because @ai-sdk/openai validates it exists, + // even though Modal's public endpoint doesn't actually need authentication + const modalClient = createOpenAI({ + baseURL: `${modalEndpoint}/v1`, + apiKey: "not-needed-for-modal", // Modal endpoint is public, but SDK requires a value + }); + console.error(`${logPrefix} GPT-OSS endpoint: ${modalEndpoint}`); + // Use .chat() to force Chat Completions API (/v1/chat/completions) + return modalClient.chat("llm"); // Model name is "llm" on vLLM server + } + + if (model === "gpt-5") { + if (!keys.openaiApiKey) { + throw new Error("OpenAI API key is required for GPT-5. Please enter your API key in the Model Settings panel."); + } + const client = createOpenAI({ apiKey: keys.openaiApiKey }); + return client("gpt-5"); + } + + if (model === "claude-4.5") { + if (!keys.anthropicApiKey) { + throw new Error("Anthropic API key is required for Claude 4.5. Please enter your API key in the Model Settings panel."); + } + const client = createAnthropic({ apiKey: keys.anthropicApiKey }); + return client("claude-sonnet-4-5"); + } + + if (model === "claude-opus") { + if (!keys.anthropicApiKey) { + throw new Error("Anthropic API key is required for Claude Opus. Please enter your API key in the Model Settings panel."); + } + const client = createAnthropic({ apiKey: keys.anthropicApiKey }); + return client("claude-opus-4-5"); + } + + if (model === "gemini-3") { + if (!keys.googleApiKey) { + throw new Error("Google API key is required for Gemini 3. Please enter your API key in the Model Settings panel."); + } + const client = createGoogleGenerativeAI({ apiKey: keys.googleApiKey }); + return client("gemini-3-pro-preview"); + } + + // Default to Grok-4-1 + if (!keys.xaiApiKey) { + throw new Error("xAI API key is required for Grok 4.1. Please enter your API key in the Model Settings panel."); + } + const client = createXai({ apiKey: keys.xaiApiKey }); + return client("grok-4-1-fast-reasoning"); +} + diff --git a/packages/eu-ai-act-mcp/tsconfig.json b/packages/eu-ai-act-mcp/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..04cbc9ec65baaeb42eafbf276775844254342c58 --- /dev/null +++ b/packages/eu-ai-act-mcp/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} + diff --git a/packages/eu-ai-act-mcp/tsup.config.ts b/packages/eu-ai-act-mcp/tsup.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b148bdb9d9674d334e2e187ae94c6baa42722ed --- /dev/null +++ b/packages/eu-ai-act-mcp/tsup.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: [ + "src/index.ts", + "src/tools/discover-organization.ts", + "src/tools/discover-ai-services.ts", + "src/tools/assess-compliance.ts", + "src/types/index.ts", + "src/schemas/index.ts", + ], + format: ["esm"], + dts: true, + sourcemap: true, + clean: true, + // Don't bundle dependencies - they'll be resolved at runtime + noExternal: [], + // Mark all node_modules as external to avoid bundling issues + external: [ + "dotenv", + "fs", + "path", + "url", + "node:fs", + "node:path", + "node:url", + "node:child_process", + "node:stream", + "@modelcontextprotocol/sdk", + "@tavily/core", + "zod", + "ai", + "@ai-sdk/openai", + "openai", + ], + outDir: "dist", +}); + diff --git a/packages/test-agent/package.json b/packages/test-agent/package.json new file mode 100644 index 0000000000000000000000000000000000000000..c069039c1ee8b3cecf4151fd58e2471046e34f6d --- /dev/null +++ b/packages/test-agent/package.json @@ -0,0 +1,23 @@ +{ + "name": "@eu-ai-act/test-agent", + "version": "0.1.0", + "description": "Test Agent for EU AI Act MCP Server", + "type": "module", + "main": "dist/index.js", + "scripts": { + "dev": "tsx src/index.ts", + "build": "tsup src/index.ts --format esm", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "ai": "^4.0.31", + "dotenv": "^16.4.7" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "tsup": "^8.3.5", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/packages/test-agent/src/index.ts b/packages/test-agent/src/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..d8f2e7b7965a27c956b74003a4f1d870581e8a6e --- /dev/null +++ b/packages/test-agent/src/index.ts @@ -0,0 +1,269 @@ +/** + * Test Agent for EU AI Act MCP Server + * Demonstrates MCP server usage and validates functionality + */ + +import { config } from "dotenv"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +// Load .env from workspace root +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootEnvPath = resolve(__dirname, "../../../.env"); +config({ path: rootEnvPath }); + +/** + * Test the MCP Server + */ +async function testMCPServer() { + console.log("🚀 Starting EU AI Act MCP Server Test Agent\n"); + + // Create MCP client + const client = new Client( + { + name: "test-agent", + version: "0.1.0", + }, + { + capabilities: {}, + } + ); + + // Connect to MCP server via stdio + const serverPath = new URL("../../eu-ai-act-mcp/src/index.ts", import.meta.url); + + const transport = new StdioClientTransport({ + command: "tsx", + args: [serverPath.pathname], + }); + + await client.connect(transport); + + console.log("✅ Connected to MCP Server\n"); + + // Test 1: List available tools + console.log("📋 Test 1: Listing available tools..."); + const toolsList = await client.listTools(); + console.log(`Found ${toolsList.tools.length} tools:`); + for (const tool of toolsList.tools) { + const desc = tool.description ? tool.description.split("\n")[0] : "No description"; + console.log(` - ${tool.name}: ${desc}`); + } + console.log(); + + // Test 2: Discover Organization (domain auto-discovery) + console.log("🏢 Test 2: Discovering organization (domain auto-discovered from research)..."); + const orgResult = await client.callTool({ + name: "discover_organization", + arguments: { + organizationName: "IBM", + context: "A technology company that provides AI and consulting solutions", + }, + }); + + console.log("Raw result:", JSON.stringify(orgResult, null, 2)); + + if (orgResult.content && Array.isArray(orgResult.content) && orgResult.content[0]?.type === "text") { + const orgData = JSON.parse(orgResult.content[0].text); + console.log("Organization Profile:"); + console.log(` Name: ${orgData.organization.name}`); + console.log(` Sector: ${orgData.organization.sector}`); + console.log(` Size: ${orgData.organization.size}`); + console.log(` AI Maturity: ${orgData.organization.aiMaturityLevel}`); + console.log(` Primary Role: ${orgData.organization.primaryRole}`); + console.log(` EU Presence: ${orgData.organization.euPresence}`); + console.log(` Compliance Deadlines: ${orgData.regulatoryContext.complianceDeadlines.length}`); + console.log(` Completeness Score: ${orgData.metadata.completenessScore}%`); + console.log(); + + // Test 3: Discover AI Services with organization context + console.log("🤖 Test 3: Discovering AI services..."); + const servicesResult = await client.callTool({ + name: "discover_ai_services", + arguments: { + organizationContext: orgData, + scope: "all", + systemNames: ["watson ai"], + }, + }); + + if (servicesResult.content && Array.isArray(servicesResult.content) && servicesResult.content[0]?.type === "text") { + const servicesData = JSON.parse(servicesResult.content[0].text); + + console.log("AI Systems Discovery Results:"); + console.log(` Total Systems: ${servicesData.riskSummary.totalCount}`); + console.log(` High-Risk: ${servicesData.riskSummary.highRiskCount}`); + console.log(` Limited Risk: ${servicesData.riskSummary.limitedRiskCount}`); + console.log(` Minimal Risk: ${servicesData.riskSummary.minimalRiskCount}`); + console.log(); + + console.log("Compliance Summary:"); + console.log(` Fully Compliant: ${servicesData.complianceSummary.fullyCompliantCount}`); + console.log(` Partially Compliant: ${servicesData.complianceSummary.partiallyCompliantCount}`); + console.log(` Non-Compliant: ${servicesData.complianceSummary.nonCompliantCount}`); + console.log(` Requires Attention: ${servicesData.complianceSummary.requiresAttention.length}`); + console.log(); + + // Display systems requiring attention + if (servicesData.complianceSummary.requiresAttention.length > 0) { + console.log("⚠️ Systems Requiring Attention:"); + for (const system of servicesData.complianceSummary.requiresAttention) { + console.log(`\n 📍 ${system.system.name}`); + console.log(` Risk Category: ${system.riskClassification.category}`); + console.log(` Risk Score: ${system.riskClassification.riskScore}/100`); + console.log(` Status: ${system.system.status}`); + console.log(` Compliance Gaps (${system.complianceStatus.identifiedGaps.length}):`); + for (const gap of system.complianceStatus.identifiedGaps) { + console.log(` - ${gap}`); + } + } + console.log(); + } + + // Display detailed system information + console.log("📊 Detailed System Information:"); + for (const system of servicesData.systems) { + console.log(`\n System: ${system.system.name}`); + console.log(` Purpose: ${system.system.intendedPurpose}`); + console.log(` Risk: ${system.riskClassification.category}`); + console.log(` Technology: ${system.technicalDetails.aiTechnology.join(", ")}`); + console.log(` Deployment: ${system.technicalDetails.deploymentModel}`); + console.log(` Human Oversight: ${system.technicalDetails.humanOversight.enabled ? "Yes" : "No"}`); + if (system.riskClassification.annexIIICategory) { + console.log(` Annex III Category: ${system.riskClassification.annexIIICategory}`); + } + } + + // Test 4: Compliance Assessment with AI + console.log("\n📋 Test 4: Running AI-powered compliance assessment..."); + console.log(" (This requires OPENAI_API_KEY to be set)"); + + try { + const complianceResult = await client.callTool({ + name: "assess_compliance", + arguments: { + organizationContext: orgData, + aiServicesContext: servicesData, + focusAreas: ["Technical Documentation", "Risk Management", "Conformity Assessment"], + generateDocumentation: true, + }, + }); + + if (complianceResult.content && Array.isArray(complianceResult.content) && complianceResult.content[0]?.type === "text") { + const complianceData = JSON.parse(complianceResult.content[0].text); + + console.log("\n🎯 Compliance Assessment Results:"); + console.log(` Overall Score: ${complianceData.assessment.overallScore}/100`); + console.log(` Risk Level: ${complianceData.assessment.riskLevel}`); + console.log(` Total Gaps: ${complianceData.assessment.gaps.length}`); + console.log(` Recommendations: ${complianceData.assessment.recommendations.length}`); + console.log(); + + // Display gaps by severity + const criticalGaps = complianceData.assessment.gaps.filter((g: any) => g.severity === "CRITICAL"); + const highGaps = complianceData.assessment.gaps.filter((g: any) => g.severity === "HIGH"); + const mediumGaps = complianceData.assessment.gaps.filter((g: any) => g.severity === "MEDIUM"); + const lowGaps = complianceData.assessment.gaps.filter((g: any) => g.severity === "LOW"); + + console.log("📊 Gap Analysis:"); + console.log(` 🔴 CRITICAL: ${criticalGaps.length}`); + console.log(` 🟠 HIGH: ${highGaps.length}`); + console.log(` 🟡 MEDIUM: ${mediumGaps.length}`); + console.log(` 🟢 LOW: ${lowGaps.length}`); + console.log(); + + // Display top 3 critical/high gaps + const topGaps = [...criticalGaps, ...highGaps].slice(0, 3); + if (topGaps.length > 0) { + console.log("⚠️ Top Priority Gaps:"); + for (const gap of topGaps) { + console.log(`\n [${gap.severity}] ${gap.category}`); + console.log(` Description: ${gap.description.substring(0, 100)}...`); + console.log(` Article: ${gap.articleReference}`); + console.log(` Effort: ${gap.remediationEffort}`); + } + console.log(); + } + + // Display top 3 recommendations + if (complianceData.assessment.recommendations.length > 0) { + console.log("💡 Top Recommendations:"); + const sortedRecs = [...complianceData.assessment.recommendations].sort((a: any, b: any) => a.priority - b.priority); + for (const rec of sortedRecs.slice(0, 3)) { + console.log(`\n Priority ${rec.priority}: ${rec.title}`); + console.log(` Article: ${rec.articleReference}`); + console.log(` Effort: ${rec.estimatedEffort}`); + console.log(` Steps: ${rec.implementationSteps.length} implementation steps`); + } + console.log(); + } + + // Display compliance by article + console.log("📖 Compliance by EU AI Act Article:"); + for (const [article, status] of Object.entries(complianceData.assessment.complianceByArticle)) { + const articleStatus = status as { compliant: boolean; gaps: string[] }; + const statusIcon = articleStatus.compliant ? "✅" : "❌"; + console.log(` ${statusIcon} ${article}: ${articleStatus.compliant ? "Compliant" : `${articleStatus.gaps.length} gap(s)`}`); + } + console.log(); + + // Display documentation templates summary + if (complianceData.documentation) { + console.log("📄 Generated Documentation Templates:"); + const docs = complianceData.documentation; + if (docs.riskManagementTemplate) console.log(" ✅ Risk Management System (Article 9)"); + if (docs.technicalDocumentation) console.log(" ✅ Technical Documentation (Article 11)"); + if (docs.conformityAssessment) console.log(" ✅ Conformity Assessment (Article 43)"); + if (docs.transparencyNotice) console.log(" ✅ Transparency Notice (Article 50)"); + if (docs.qualityManagementSystem) console.log(" ✅ Quality Management System (Article 17)"); + if (docs.humanOversightProcedure) console.log(" ✅ Human Oversight Procedure (Article 14)"); + if (docs.dataGovernancePolicy) console.log(" ✅ Data Governance Policy (Article 10)"); + if (docs.incidentReportingProcedure) console.log(" ✅ Incident Reporting Procedure"); + console.log(); + } + + // Display reasoning summary + if (complianceData.reasoning) { + console.log("🧠 AI Reasoning Summary:"); + const reasoningPreview = complianceData.reasoning.substring(0, 500); + console.log(` ${reasoningPreview}${complianceData.reasoning.length > 500 ? "..." : ""}`); + console.log(); + } + + // Display metadata + console.log("📋 Assessment Metadata:"); + console.log(` Assessment Date: ${complianceData.metadata.assessmentDate}`); + console.log(` Model Used: ${complianceData.metadata.modelUsed}`); + console.log(` Systems Assessed: ${complianceData.metadata.systemsAssessed.join(", ")}`); + console.log(` Focus Areas: ${complianceData.metadata.focusAreas.join(", ")}`); + } + } catch (complianceError: any) { + if (complianceError.message?.includes("OPENAI_API_KEY")) { + console.log("⚠️ Skipping compliance assessment: OPENAI_API_KEY not set"); + console.log(" Set OPENAI_API_KEY environment variable to enable AI-powered compliance assessment"); + } else { + console.error("❌ Compliance assessment failed:", complianceError.message); + } + } + } + } + + console.log("\n✅ All tests completed successfully!"); + console.log("\n📚 MCP Server is ready for use with:"); + console.log(" - Claude Desktop"); + console.log(" - ChatGPT Apps SDK"); + console.log(" - Any MCP-compliant client"); + + await client.close(); + process.exit(0); +} + +// Run tests +testMCPServer().catch((error) => { + console.error("❌ Test failed:", error); + process.exit(1); +}); + diff --git a/packages/test-agent/tsconfig.json b/packages/test-agent/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..58f90e3d80ca846c8258bab1d6fc6e36626fb9e8 --- /dev/null +++ b/packages/test-agent/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tooling/typescript/base.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "target": "ES2022", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b38daffdcbb9a44caeb564c51fe34e6f17a4e990 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5549 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +overrides: + '@auth/core': 0.18.0 + +importers: + + .: + dependencies: + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + devDependencies: + '@biomejs/biome': + specifier: ^2.3.8 + version: 2.3.8 + '@turbo/gen': + specifier: ^2.6.1 + version: 2.6.1(@types/node@22.19.1)(typescript@5.9.3) + turbo: + specifier: ^2.6.1 + version: 2.6.1 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + apps/eu-ai-act-agent: + dependencies: + '@ai-sdk/anthropic': + specifier: ^2.0.50 + version: 2.0.50(zod@3.25.76) + '@ai-sdk/google': + specifier: 3.0.0-beta.62 + version: 3.0.0-beta.62(zod@3.25.76) + '@ai-sdk/mcp': + specifier: ^0.0.11 + version: 0.0.11(zod@3.25.76) + '@ai-sdk/openai': + specifier: ^2.0.74 + version: 2.0.74(zod@3.25.76) + '@ai-sdk/provider-utils': + specifier: ^2.2.8 + version: 2.2.8(zod@3.25.76) + '@ai-sdk/xai': + specifier: ^2.0.39 + version: 2.0.39(zod@3.25.76) + '@eu-ai-act/mcp-server': + specifier: workspace:* + version: link:../../packages/eu-ai-act-mcp + '@modelcontextprotocol/sdk': + specifier: ^1.23.0 + version: 1.23.0(zod@3.25.76) + ai: + specifier: ^5.0.104 + version: 5.0.104(zod@3.25.76) + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 + express: + specifier: ^4.21.2 + version: 4.21.2 + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/cors': + specifier: ^2.8.17 + version: 2.8.19 + '@types/express': + specifier: ^5.0.5 + version: 5.0.5 + '@types/node': + specifier: ^22.10.2 + version: 22.19.1 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + tsx: + specifier: ^4.20.6 + version: 4.20.6 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + + packages/eu-ai-act-mcp: + dependencies: + '@ai-sdk/anthropic': + specifier: ^2.0.50 + version: 2.0.50(zod@3.25.76) + '@ai-sdk/google': + specifier: ^2.0.44 + version: 2.0.44(zod@3.25.76) + '@ai-sdk/openai': + specifier: ^2.0.74 + version: 2.0.74(zod@3.25.76) + '@ai-sdk/xai': + specifier: ^2.0.39 + version: 2.0.39(zod@3.25.76) + '@modelcontextprotocol/sdk': + specifier: ^1.23.0 + version: 1.23.0(zod@3.25.76) + '@tavily/core': + specifier: ^0.5.13 + version: 0.5.13 + ai: + specifier: ^5.0.104 + version: 5.0.104(zod@3.25.76) + colorthief: + specifier: ^2.6.0 + version: 2.6.0 + openai: + specifier: ^6.9.1 + version: 6.9.1(zod@3.25.76) + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/express': + specifier: ^5.0.5 + version: 5.0.5 + '@types/node': + specifier: ^22.10.2 + version: 22.19.1 + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + tsx: + specifier: ^4.20.6 + version: 4.20.6 + + packages/test-agent: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.0.4 + version: 1.23.0(zod@3.25.76) + ai: + specifier: ^4.0.31 + version: 4.3.19(react@19.2.0)(zod@3.25.76) + dotenv: + specifier: ^16.4.7 + version: 16.6.1 + devDependencies: + '@types/node': + specifier: ^22.10.2 + version: 22.19.1 + tsup: + specifier: ^8.3.5 + version: 8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + tsx: + specifier: ^4.19.2 + version: 4.20.6 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + + tooling/github: {} + + tooling/tailwind: + dependencies: + autoprefixer: + specifier: ^10.4.16 + version: 10.4.22(postcss@8.5.6) + tailwindcss: + specifier: 3.3.5 + version: 3.3.5(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@3.3.5(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))) + devDependencies: + '@biomejs/biome': + specifier: ^1.5.3 + version: 1.9.4 + '@decode/tsconfig': + specifier: workspace:^0.1.0 + version: link:../typescript + typescript: + specifier: ^5.2.2 + version: 5.9.3 + + tooling/typescript: {} + +packages: + + '@ai-sdk/anthropic@2.0.50': + resolution: {integrity: sha512-21PaHfoLmouOXXNINTsZJsMw+wE5oLR2He/1kq/sKokTVKyq7ObGT1LDk6ahwxaz/GoaNaGankMh+EgVcdv2Cw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/gateway@2.0.17': + resolution: {integrity: sha512-oVAG6q72KsjKlrYdLhWjRO7rcqAR8CjokAbYuyVZoCO4Uh2PH/VzZoxZav71w2ipwlXhHCNaInGYWNs889MMDA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@2.0.44': + resolution: {integrity: sha512-c5dck36FjqiVoeeMJQLTEmUheoURcGTU/nBT6iJu8/nZiKFT/y8pD85KMDRB7RerRYaaQOtslR2d6/5PditiRw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@3.0.0-beta.62': + resolution: {integrity: sha512-V4rU2daTocs36DJ23kp0/XiC8CJJt1fZl/OENw5f/ZkrDalQO57D3ktFcHX/qT/htuVbhGvxdrz3r2yQ8yVs+Q==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/mcp@0.0.11': + resolution: {integrity: sha512-n0oZsxhPdMaXhAn6LrpMpxABufmeSatfhR3epbrCjJcLEHPOucKwQciwE8CTgIGgwBxHNy1FFLZmcFI77JmJfg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai-compatible@1.0.28': + resolution: {integrity: sha512-yKubDxLYtXyGUzkr9lNStf/lE/I+Okc8tmotvyABhsQHHieLKk6oV5fJeRJxhr67Ejhg+FRnwUOxAmjRoFM4dA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@2.0.74': + resolution: {integrity: sha512-vvsL7rGoBEyQIePs630p31ebLeF+xxwLOrRKeIArHko8w7Wh9Kj3wL4Ns+PCzrEpAij31OKKDcxLQ1dSIg/qMw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@2.2.8': + resolution: {integrity: sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/provider-utils@3.0.18': + resolution: {integrity: sha512-ypv1xXMsgGcNKUP+hglKqtdDuMg68nWHucPPAhIENrbFAI+xCHiqPVN8Zllxyv1TNZwGWUghPxJXU+Mqps0YRQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@4.0.0-beta.40': + resolution: {integrity: sha512-5345iQxWV1coKbs85vkrThsEmiFcIkgqMZUKFrVDM7E4FRqgsnWtn4394/RnShOKRun5K7TWIYCLz3GfRGy/Ig==} + engines: {node: '>=18'} + peerDependencies: + '@valibot/to-json-schema': ^1.3.0 + arktype: ^2.1.22 + effect: ^3.18.4 + zod: ^3.25.76 || ^4.1.8 + peerDependenciesMeta: + '@valibot/to-json-schema': + optional: true + arktype: + optional: true + effect: + optional: true + + '@ai-sdk/provider@1.1.3': + resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} + engines: {node: '>=18'} + + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + engines: {node: '>=18'} + + '@ai-sdk/provider@3.0.0-beta.22': + resolution: {integrity: sha512-Ss0tgCZwzccS6MREhjAI28lkAV5PfJnoBFbv8mas74Cs5OseFKqxg3dEd1lRL8mf4Qapu1xpBLkV4/ldSShSdA==} + engines: {node: '>=18'} + + '@ai-sdk/react@1.2.12': + resolution: {integrity: sha512-jK1IZZ22evPZoQW3vlkZ7wvjYGYF+tRBKXtrcolduIkQ/m/sOAVcVeVDUDvh1T91xCnWCdUGCPZg2avZ90mv3g==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + zod: + optional: true + + '@ai-sdk/ui-utils@1.2.11': + resolution: {integrity: sha512-3zcwCc8ezzFlwp3ZD15wAPjf2Au4s3vAbKsXQVyhxODHcmu0iyPO2Eua6D/vicq/AUm/BAo60r97O6HU+EI0+w==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.23.8 + + '@ai-sdk/xai@2.0.39': + resolution: {integrity: sha512-EtRRHpPb3J6qY8y9C9p1g3FdF8dl6SocmfyS418g+PesK9/bIAbJYWQStdWpJXF/d9VfzeoOp1IhcBgKotAn+A==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@babel/runtime-corejs3@7.28.4': + resolution: {integrity: sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==} + engines: {node: '>=6.9.0'} + + '@biomejs/biome@1.9.4': + resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/biome@2.3.8': + resolution: {integrity: sha512-Qjsgoe6FEBxWAUzwFGFrB+1+M8y/y5kwmg5CHac+GSVOdmOIqsAiXM5QMVGZJ1eCUCLlPZtq4aFAQ0eawEUuUA==} + engines: {node: '>=14.21.3'} + hasBin: true + + '@biomejs/cli-darwin-arm64@1.9.4': + resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-arm64@2.3.8': + resolution: {integrity: sha512-HM4Zg9CGQ3txTPflxD19n8MFPrmUAjaC7PQdLkugeeC0cQ+PiVrd7i09gaBS/11QKsTDBJhVg85CEIK9f50Qww==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [darwin] + + '@biomejs/cli-darwin-x64@1.9.4': + resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-darwin-x64@2.3.8': + resolution: {integrity: sha512-lUDQ03D7y/qEao7RgdjWVGCu+BLYadhKTm40HkpJIi6kn8LSv5PAwRlew/DmwP4YZ9ke9XXoTIQDO1vAnbRZlA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [darwin] + + '@biomejs/cli-linux-arm64-musl@1.9.4': + resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64-musl@2.3.8': + resolution: {integrity: sha512-PShR4mM0sjksUMyxbyPNMxoKFPVF48fU8Qe8Sfx6w6F42verbwRLbz+QiKNiDPRJwUoMG1nPM50OBL3aOnTevA==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@1.9.4': + resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-arm64@2.3.8': + resolution: {integrity: sha512-Uo1OJnIkJgSgF+USx970fsM/drtPcQ39I+JO+Fjsaa9ZdCN1oysQmy6oAGbyESlouz+rzEckLTF6DS7cWse95g==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@1.9.4': + resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64-musl@2.3.8': + resolution: {integrity: sha512-YGLkqU91r1276uwSjiUD/xaVikdxgV1QpsicT0bIA1TaieM6E5ibMZeSyjQ/izBn4tKQthUSsVZacmoJfa3pDA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@1.9.4': + resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-linux-x64@2.3.8': + resolution: {integrity: sha512-QDPMD5bQz6qOVb3kiBui0zKZXASLo0NIQ9JVJio5RveBEFgDgsvJFUvZIbMbUZT3T00M/1wdzwWXk4GIh0KaAw==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [linux] + + '@biomejs/cli-win32-arm64@1.9.4': + resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-arm64@2.3.8': + resolution: {integrity: sha512-H4IoCHvL1fXKDrTALeTKMiE7GGWFAraDwBYFquE/L/5r1927Te0mYIGseXi4F+lrrwhSWbSGt5qPFswNoBaCxg==} + engines: {node: '>=14.21.3'} + cpu: [arm64] + os: [win32] + + '@biomejs/cli-win32-x64@1.9.4': + resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@biomejs/cli-win32-x64@2.3.8': + resolution: {integrity: sha512-RguzimPoZWtBapfKhKjcWXBVI91tiSprqdBYu7tWhgN8pKRZhw24rFeNZTNf6UiBfjCYCi9eFQs/JzJZIhuK4w==} + engines: {node: '>=14.21.3'} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.7.1': + resolution: {integrity: sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==} + + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@lokesh.dhakar/quantize@1.4.0': + resolution: {integrity: sha512-+//cqVWKis//t0YH62EDtwaFSPG/CDtYNg4CZmzNmG2d5W17Iu3fuDAdpQXCDHUDrrU9q0veze4A7tPZXlR/mg==} + + '@modelcontextprotocol/sdk@1.23.0': + resolution: {integrity: sha512-MCGd4K9aZKvuSqdoBkdMvZNcYXCkZRYVs/Gh92mdV5IHbctX9H9uIvd4X93+9g8tBbXv08sxc/QHXTzf8y65bA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@tavily/core@0.5.13': + resolution: {integrity: sha512-H7QzDDQews4r7HCrCnbAM8RyqlQt148G0UnjfCHMcOOrsP+8EwDqeOP4G47RrFGXEiS/jbTftGrDRAQnHUGnqA==} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@tsconfig/node10@1.0.12': + resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@turbo/gen@2.6.1': + resolution: {integrity: sha512-MgWIZ7nPr/X8ty4zVwaquoX+XXioufUZAG77G01C5TVJF/7iKrMAk/l0hgn99VoCb+nJL9qD8MA0HrJBgXyn8w==} + hasBin: true + + '@turbo/workspaces@2.6.1': + resolution: {integrity: sha512-RqCfreuW34qN4G6OKtXrCLzvfsiBy6oGN67LhM/Fvk6b7XWn6GCBwTe0JM+CrH0uCiQbgYa6HgnWcUlTxIvYQw==} + hasBin: true + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/diff-match-patch@1.0.36': + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/express-serve-static-core@5.1.0': + resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} + + '@types/express@5.0.5': + resolution: {integrity: sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==} + + '@types/glob@7.2.0': + resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/inquirer@6.5.0': + resolution: {integrity: sha512-rjaYQ9b9y/VFGOpqBEXRavc3jh0a+e6evAbI31tMda8VlPaSy0AZJfXsvmIe3wklc7W6C3zCSfleuMXR7NOyXw==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/minimatch@6.0.0': + resolution: {integrity: sha512-zmPitbQ8+6zNutpwgcQuLcsEpn/Cj54Kbn7L5pX0Os5kdWplB7xPgEh/g+SWOB/qmows2gpuCaPyduq8ZZRnxA==} + deprecated: This is a stub types definition. minimatch provides its own type definitions, so you do not need this installed. + + '@types/ndarray@1.0.14': + resolution: {integrity: sha512-oANmFZMnFQvb219SSBIhI1Ih/r4CvHDOzkWyJS/XRqkMrGH5/kaPSA1hQhdIBzouaE+5KpE/f5ylI9cujmckQg==} + + '@types/node@22.19.1': + resolution: {integrity: sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@types/through@0.0.33': + resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} + + '@types/tinycolor2@1.4.6': + resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} + + '@vercel/oidc@3.0.5': + resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} + engines: {node: '>= 20'} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + aggregate-error@3.1.0: + resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} + engines: {node: '>=8'} + + ai@4.3.19: + resolution: {integrity: sha512-dIE2bfNpqHN3r6IINp9znguYdhIOheKW2LDigAMrgt/upT3B8eBGPSCblENvaZGoq+hxaN9fSMzjWpbqloP+7Q==} + engines: {node: '>=18'} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + zod: ^3.23.8 + peerDependenciesMeta: + react: + optional: true + + ai@5.0.104: + resolution: {integrity: sha512-MZOkL9++nY5PfkpWKBR3Rv+Oygxpb9S16ctv8h91GvrSif7UnNEdPMVZe3bUyMd2djxf0AtBk/csBixP0WwWZQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + autoprefixer@10.4.22: + resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.8.32: + resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} + hasBin: true + + basic-ftp@5.0.5: + resolution: {integrity: sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==} + engines: {node: '>=10.0.0'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + engines: {node: '>=18'} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.0: + resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camel-case@3.0.0: + resolution: {integrity: sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==} + + camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + + caniuse-lite@1.0.30001757: + resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + change-case@3.1.0: + resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==} + + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + + chardet@2.1.1: + resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + clean-stack@2.2.0: + resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-width@3.0.0: + resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} + engines: {node: '>= 10'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + colorthief@2.6.0: + resolution: {integrity: sha512-yL3B7laeOr4kH9XasFF5rl+9Taz+Pmt/CRbaTI6XepZFyQvk4K/abaGKIAsngVpxKkgFeoJ2IwdRpS228icrig==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + constant-case@2.0.0: + resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + core-js-pure@3.47.0: + resolution: {integrity: sha512-BcxeDbzUrRnXGYIVAGFtcGQVNpFcUhVjr6W7F8XktvQW2iJP9e66GP6xdKotCRFlrxBvNIBrhwKteRXqMV86Nw==} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cwise-compiler@1.1.3: + resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + del@5.1.0: + resolution: {integrity: sha512-wH9xOVHnczo9jN2IW68BabcecVPxacIA3g/7z6vhSU/4stOKQzeCRK0yD0A24WiAAUJmmVpWqrERcTxnLo3AnA==} + engines: {node: '>=8'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + + dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + + dot-case@2.1.1: + resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + electron-to-chromium@1.5.262: + resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + + file-type@16.5.4: + resolution: {integrity: sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==} + engines: {node: '>=10'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globby@10.0.2: + resolution: {integrity: sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg==} + engines: {node: '>=8'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + gradient-string@2.0.2: + resolution: {integrity: sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==} + engines: {node: '>=10'} + + handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + header-case@1.0.1: + resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inquirer@7.3.3: + resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} + engines: {node: '>=8.0.0'} + + inquirer@8.2.7: + resolution: {integrity: sha512-UjOaSel/iddGZJ5xP/Eixh6dY1XghiBw4XK13rCCIJcJfyhhoul/7KhLLUGtebEj6GDYM6Vnx/mVsjx2L/mFIA==} + engines: {node: '>=12.0.0'} + + iota-array@1.0.0: + resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==} + + ip-address@10.1.0: + resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-buffer@1.1.6: + resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-lower-case@1.1.3: + resolution: {integrity: sha512-+5A1e/WJpLLXZEDlgz4G//WYSHyQBD32qa4Jd3Lw06qQlv3fJHnp3YIHjTQSGzHMgzmVKz2ZP3rBxTHkPw/lxA==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-cwd@2.2.0: + resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} + engines: {node: '>=6'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-upper-case@1.1.2: + resolution: {integrity: sha512-GQYSJMgfeAmVwh9ixyk888l7OIhNAGKtY6QA+IrWlu9MDTCaXmeozOZ2S9Knj7bQwBO/H6J2kb+pbyTUiMNbsw==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@1.21.7: + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} + hasBin: true + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + jsondiffpatch@0.6.0: + resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.get@4.4.2: + resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} + deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + log-symbols@3.0.0: + resolution: {integrity: sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==} + engines: {node: '>=8'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lower-case-first@1.0.2: + resolution: {integrity: sha512-UuxaYakO7XeONbKrZf5FEgkantPf5DUqDayzP5VXZrtRPdH86s4kN47I8B3TW10S4QKiE3ziHNf3kRN//okHjA==} + + lower-case@1.1.4: + resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + ndarray-ops@1.2.2: + resolution: {integrity: sha512-BppWAFRjMYF7N/r6Ie51q6D4fs0iiGmeXIACKY66fLpnwIui3Wc3CXiD/30mgLbDjPpSLrsqcp3Z62+IcHZsDw==} + + ndarray-pixels@4.1.0: + resolution: {integrity: sha512-xKPI4zXJ2pkUcVX24zIN1AWqqPWvRWWhRuO6PlY4EdB2VNRauNwA6rDdsAQG/ldQp0sU7nTXgPR/io1duy3Zyg==} + + ndarray@1.0.19: + resolution: {integrity: sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + netmask@2.0.2: + resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} + engines: {node: '>= 0.4.0'} + + no-case@2.3.2: + resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + + node-plop@0.26.3: + resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==} + engines: {node: '>=8.9.4'} + + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + openai@6.9.1: + resolution: {integrity: sha512-vQ5Rlt0ZgB3/BNmTa7bIijYFhz3YBceAA3Z4JuoMSBftBF9YqFHIEhZakSs+O/Ad7EaoEimZvHxD5ylRjN11Lg==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + ora@4.1.1: + resolution: {integrity: sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==} + engines: {node: '>=8'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + + p-map@3.0.0: + resolution: {integrity: sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==} + engines: {node: '>=8'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + param-case@2.1.1: + resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + pascal-case@2.0.1: + resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==} + + path-case@2.1.1: + resolution: {integrity: sha512-Ou0N05MioItesaLr9q8TtHVWmJ6fxWdqKB2RohFmNWVyJ+2zeKIeDNWAN6B/Pe7wpzWChhZX6nONYmOnMeJQ/Q==} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + peek-readable@4.1.0: + resolution: {integrity: sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==} + engines: {node: '>=8'} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-import@15.1.0: + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} + engines: {node: '>=14.0.0'} + peerDependencies: + postcss: ^8.0.0 + + postcss-js@4.1.0: + resolution: {integrity: sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + + postcss-load-config@4.0.2: + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss-nested@6.2.0: + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + readable-web-to-node-stream@3.0.4: + resolution: {integrity: sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==} + engines: {node: '>=8'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + registry-auth-token@3.3.2: + resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==} + + registry-url@3.1.0: + resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + run-async@2.4.1: + resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==} + engines: {node: '>=0.12.0'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@6.6.7: + resolution: {integrity: sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==} + engines: {npm: '>=2.0.0'} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + secure-json-parse@2.7.0: + resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} + + semver@7.6.2: + resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + sentence-case@2.1.1: + resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + snake-case@2.1.0: + resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.7: + resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + strtok3@6.3.0: + resolution: {integrity: sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==} + engines: {node: '>=10'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + swap-case@1.1.2: + resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==} + + swr@2.3.7: + resolution: {integrity: sha512-ZEquQ82QvalqTxhBVv/DlAg2mbmUjF4UgpPg9wwk4ufb9rQnZXh1iKyyKBqV6bQGu1Ie7L1QwSYO07qFIa1p+g==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + tailwindcss-animate@1.0.7: + resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders' + + tailwindcss@3.3.5: + resolution: {integrity: sha512-5SEZU4J7pxZgSkv7FP1zY8i2TIAOooNZ1e/OGtxIEv6GltpoiXUqWvLy89+a10qYTB1N5Ifkuw9lqQkN9sscvA==} + engines: {node: '>=14.0.0'} + hasBin: true + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + throttleit@2.1.0: + resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} + engines: {node: '>=18'} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinygradient@1.1.5: + resolution: {integrity: sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==} + + title-case@2.1.1: + resolution: {integrity: sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==} + + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@4.2.1: + resolution: {integrity: sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==} + engines: {node: '>=10'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + + turbo-darwin-64@2.6.1: + resolution: {integrity: sha512-Dm0HwhyZF4J0uLqkhUyCVJvKM9Rw7M03v3J9A7drHDQW0qAbIGBrUijQ8g4Q9Cciw/BXRRd8Uzkc3oue+qn+ZQ==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.6.1: + resolution: {integrity: sha512-U0PIPTPyxdLsrC3jN7jaJUwgzX5sVUBsKLO7+6AL+OASaa1NbT1pPdiZoTkblBAALLP76FM0LlnsVQOnmjYhyw==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.6.1: + resolution: {integrity: sha512-eM1uLWgzv89bxlK29qwQEr9xYWBhmO/EGiH22UGfq+uXr+QW1OvNKKMogSN65Ry8lElMH4LZh0aX2DEc7eC0Mw==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.6.1: + resolution: {integrity: sha512-MFFh7AxAQAycXKuZDrbeutfWM5Ep0CEZ9u7zs4Hn2FvOViTCzIfEhmuJou3/a5+q5VX1zTxQrKGy+4Lf5cdpsA==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.6.1: + resolution: {integrity: sha512-buq7/VAN7KOjMYi4tSZT5m+jpqyhbRU2EUTTvp6V0Ii8dAkY2tAAjQN1q5q2ByflYWKecbQNTqxmVploE0LVwQ==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.6.1: + resolution: {integrity: sha512-7w+AD5vJp3R+FB0YOj1YJcNcOOvBior7bcHTodqp90S3x3bLgpr7tE6xOea1e8JkP7GK6ciKVUpQvV7psiwU5Q==} + cpu: [arm64] + os: [win32] + + turbo@2.6.1: + resolution: {integrity: sha512-qBwXXuDT3rA53kbNafGbT5r++BrhRgx3sAo0cHoDAeG9g1ItTmUMgltz3Hy7Hazy1ODqNpR+C7QwqL6DYB52yA==} + hasBin: true + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uniq@1.0.1: + resolution: {integrity: sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + update-check@1.5.4: + resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==} + + upper-case-first@1.1.2: + resolution: {integrity: sha512-wINKYvI3Db8dtjikdAqoBbZoP6Q+PZUyfMR7pmwHzjC2quzSkUq5DmPrTtPEqHaz8AGtmsB4TqwapMTM1QAQOQ==} + + upper-case@1.1.3: + resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + validate-npm-package-name@5.0.1: + resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + zod-to-json-schema@3.25.0: + resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@ai-sdk/anthropic@2.0.50(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.18(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/gateway@2.0.17(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.18(zod@3.25.76) + '@vercel/oidc': 3.0.5 + zod: 3.25.76 + + '@ai-sdk/google@2.0.44(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.18(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/google@3.0.0-beta.62(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.0-beta.22 + '@ai-sdk/provider-utils': 4.0.0-beta.40(zod@3.25.76) + zod: 3.25.76 + transitivePeerDependencies: + - '@valibot/to-json-schema' + - arktype + - effect + + '@ai-sdk/mcp@0.0.11(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.18(zod@3.25.76) + pkce-challenge: 5.0.1 + zod: 3.25.76 + + '@ai-sdk/openai-compatible@1.0.28(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.18(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai@2.0.74(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.18(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@2.2.8(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + nanoid: 3.3.11 + secure-json-parse: 2.7.0 + zod: 3.25.76 + + '@ai-sdk/provider-utils@3.0.18(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider-utils@4.0.0-beta.40(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 3.0.0-beta.22 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@1.1.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/provider@3.0.0-beta.22': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/react@1.2.12(react@19.2.0)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + react: 19.2.0 + swr: 2.3.7(react@19.2.0) + throttleit: 2.1.0 + optionalDependencies: + zod: 3.25.76 + + '@ai-sdk/ui-utils@1.2.11(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + + '@ai-sdk/xai@2.0.39(zod@3.25.76)': + dependencies: + '@ai-sdk/openai-compatible': 1.0.28(zod@3.25.76) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.18(zod@3.25.76) + zod: 3.25.76 + + '@alloc/quick-lru@5.2.0': {} + + '@babel/runtime-corejs3@7.28.4': + dependencies: + core-js-pure: 3.47.0 + + '@biomejs/biome@1.9.4': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 1.9.4 + '@biomejs/cli-darwin-x64': 1.9.4 + '@biomejs/cli-linux-arm64': 1.9.4 + '@biomejs/cli-linux-arm64-musl': 1.9.4 + '@biomejs/cli-linux-x64': 1.9.4 + '@biomejs/cli-linux-x64-musl': 1.9.4 + '@biomejs/cli-win32-arm64': 1.9.4 + '@biomejs/cli-win32-x64': 1.9.4 + + '@biomejs/biome@2.3.8': + optionalDependencies: + '@biomejs/cli-darwin-arm64': 2.3.8 + '@biomejs/cli-darwin-x64': 2.3.8 + '@biomejs/cli-linux-arm64': 2.3.8 + '@biomejs/cli-linux-arm64-musl': 2.3.8 + '@biomejs/cli-linux-x64': 2.3.8 + '@biomejs/cli-linux-x64-musl': 2.3.8 + '@biomejs/cli-win32-arm64': 2.3.8 + '@biomejs/cli-win32-x64': 2.3.8 + + '@biomejs/cli-darwin-arm64@1.9.4': + optional: true + + '@biomejs/cli-darwin-arm64@2.3.8': + optional: true + + '@biomejs/cli-darwin-x64@1.9.4': + optional: true + + '@biomejs/cli-darwin-x64@2.3.8': + optional: true + + '@biomejs/cli-linux-arm64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64-musl@2.3.8': + optional: true + + '@biomejs/cli-linux-arm64@1.9.4': + optional: true + + '@biomejs/cli-linux-arm64@2.3.8': + optional: true + + '@biomejs/cli-linux-x64-musl@1.9.4': + optional: true + + '@biomejs/cli-linux-x64-musl@2.3.8': + optional: true + + '@biomejs/cli-linux-x64@1.9.4': + optional: true + + '@biomejs/cli-linux-x64@2.3.8': + optional: true + + '@biomejs/cli-win32-arm64@1.9.4': + optional: true + + '@biomejs/cli-win32-arm64@2.3.8': + optional: true + + '@biomejs/cli-win32-x64@1.9.4': + optional: true + + '@biomejs/cli-win32-x64@2.3.8': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.7.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/aix-ppc64@0.27.0': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.27.0': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-arm@0.27.0': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/android-x64@0.27.0': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.27.0': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.27.0': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.27.0': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.27.0': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.27.0': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-arm@0.27.0': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.27.0': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.27.0': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.27.0': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.27.0': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.27.0': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.27.0': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/linux-x64@0.27.0': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.27.0': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.27.0': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.27.0': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.27.0': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.27.0': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.27.0': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.27.0': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.27.0': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@esbuild/win32-x64@0.27.0': + optional: true + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.7.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@inquirer/external-editor@1.0.3(@types/node@22.19.1)': + dependencies: + chardet: 2.1.1 + iconv-lite: 0.7.0 + optionalDependencies: + '@types/node': 22.19.1 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lokesh.dhakar/quantize@1.4.0': {} + + '@modelcontextprotocol/sdk@1.23.0(zod@3.25.76)': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.1.0 + express-rate-limit: 7.5.1(express@5.1.0) + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + transitivePeerDependencies: + - supports-color + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@opentelemetry/api@1.9.0': {} + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@standard-schema/spec@1.0.0': {} + + '@tavily/core@0.5.13': + dependencies: + axios: 1.13.2 + https-proxy-agent: 7.0.6 + js-tiktoken: 1.0.21 + transitivePeerDependencies: + - debug + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@tsconfig/node10@1.0.12': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@turbo/gen@2.6.1(@types/node@22.19.1)(typescript@5.9.3)': + dependencies: + '@turbo/workspaces': 2.6.1(@types/node@22.19.1) + commander: 10.0.1 + fs-extra: 10.1.0 + inquirer: 8.2.7(@types/node@22.19.1) + minimatch: 9.0.5 + node-plop: 0.26.3 + picocolors: 1.0.1 + proxy-agent: 6.5.0 + ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + update-check: 1.5.4 + validate-npm-package-name: 5.0.1 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + - supports-color + - typescript + + '@turbo/workspaces@2.6.1(@types/node@22.19.1)': + dependencies: + commander: 10.0.1 + execa: 5.1.1 + fast-glob: 3.3.3 + fs-extra: 10.1.0 + gradient-string: 2.0.2 + inquirer: 8.2.7(@types/node@22.19.1) + js-yaml: 4.1.1 + ora: 4.1.1 + picocolors: 1.0.1 + semver: 7.6.2 + update-check: 1.5.4 + transitivePeerDependencies: + - '@types/node' + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.19.1 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.19.1 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 22.19.1 + + '@types/diff-match-patch@1.0.36': {} + + '@types/estree@1.0.8': {} + + '@types/express-serve-static-core@5.1.0': + dependencies: + '@types/node': 22.19.1 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.5': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.0 + '@types/serve-static': 1.15.10 + + '@types/glob@7.2.0': + dependencies: + '@types/minimatch': 6.0.0 + '@types/node': 22.19.1 + + '@types/http-errors@2.0.5': {} + + '@types/inquirer@6.5.0': + dependencies: + '@types/through': 0.0.33 + rxjs: 6.6.7 + + '@types/mime@1.3.5': {} + + '@types/minimatch@6.0.0': + dependencies: + minimatch: 9.0.5 + + '@types/ndarray@1.0.14': {} + + '@types/node@22.19.1': + dependencies: + undici-types: 6.21.0 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 22.19.1 + + '@types/send@1.2.1': + dependencies: + '@types/node': 22.19.1 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.19.1 + '@types/send': 0.17.6 + + '@types/through@0.0.33': + dependencies: + '@types/node': 22.19.1 + + '@types/tinycolor2@1.4.6': {} + + '@vercel/oidc@3.0.5': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + agent-base@7.1.4: {} + + aggregate-error@3.1.0: + dependencies: + clean-stack: 2.2.0 + indent-string: 4.0.0 + + ai@4.3.19(react@19.2.0)(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 1.1.3 + '@ai-sdk/provider-utils': 2.2.8(zod@3.25.76) + '@ai-sdk/react': 1.2.12(react@19.2.0)(zod@3.25.76) + '@ai-sdk/ui-utils': 1.2.11(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + jsondiffpatch: 0.6.0 + zod: 3.25.76 + optionalDependencies: + react: 19.2.0 + + ai@5.0.104(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 2.0.17(zod@3.25.76) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.18(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + any-promise@1.3.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + arg@4.1.3: {} + + arg@5.0.2: {} + + argparse@2.0.1: {} + + array-flatten@1.1.1: {} + + array-union@2.1.0: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + asynckit@0.4.0: {} + + autoprefixer@10.4.22(postcss@8.5.6): + dependencies: + browserslist: 4.28.0 + caniuse-lite: 1.0.30001757 + fraction.js: 5.3.4 + normalize-range: 0.1.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.8.32: {} + + basic-ftp@5.0.5: {} + + binary-extensions@2.3.0: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.1: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.0: + dependencies: + baseline-browser-mapping: 2.8.32 + caniuse-lite: 1.0.30001757 + electron-to-chromium: 1.5.262 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.28.0) + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-require@5.1.0(esbuild@0.27.0): + dependencies: + esbuild: 0.27.0 + load-tsconfig: 0.2.5 + + bytes@3.1.2: {} + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camel-case@3.0.0: + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + + camelcase-css@2.0.1: {} + + caniuse-lite@1.0.30001757: {} + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@5.6.2: {} + + change-case@3.1.0: + dependencies: + camel-case: 3.0.0 + constant-case: 2.0.0 + dot-case: 2.1.1 + header-case: 1.0.1 + is-lower-case: 1.1.3 + is-upper-case: 1.1.2 + lower-case: 1.1.4 + lower-case-first: 1.0.2 + no-case: 2.3.2 + param-case: 2.1.1 + pascal-case: 2.0.1 + path-case: 2.1.1 + sentence-case: 2.1.1 + snake-case: 2.1.0 + swap-case: 1.1.2 + title-case: 2.1.1 + upper-case: 1.1.3 + upper-case-first: 1.1.2 + + chardet@0.7.0: {} + + chardet@2.1.1: {} + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + clean-stack@2.2.0: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-width@3.0.0: {} + + clone@1.0.4: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.3: {} + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + colorthief@2.6.0: + dependencies: + '@lokesh.dhakar/quantize': 1.4.0 + file-type: 16.5.4 + ndarray-pixels: 4.1.0 + sharp: 0.33.5 + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + constant-case@2.0.0: + dependencies: + snake-case: 2.1.0 + upper-case: 1.1.3 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.1: {} + + cookie@0.7.2: {} + + core-js-pure@3.47.0: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + cwise-compiler@1.1.3: + dependencies: + uniq: 1.0.1 + + data-uri-to-buffer@6.0.2: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-extend@0.6.0: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + del@5.1.0: + dependencies: + globby: 10.0.2 + graceful-fs: 4.2.11 + is-glob: 4.0.3 + is-path-cwd: 2.2.0 + is-path-inside: 3.0.3 + p-map: 3.0.0 + rimraf: 3.0.2 + slash: 3.0.0 + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + dequal@2.0.3: {} + + destroy@1.2.0: {} + + detect-libc@2.1.2: {} + + didyoumean@1.2.2: {} + + diff-match-patch@1.0.5: {} + + diff@4.0.2: {} + + dir-glob@3.0.1: + dependencies: + path-type: 4.0.0 + + dlv@1.1.3: {} + + dot-case@2.1.1: + dependencies: + no-case: 2.3.2 + + dotenv@16.6.1: {} + + dotenv@17.2.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + electron-to-chromium@1.5.262: {} + + emoji-regex@8.0.0: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@1.0.5: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + events@3.3.0: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + express-rate-limit@7.5.1(express@5.1.0): + dependencies: + express: 5.1.0 + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.1 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-uri@3.1.0: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + + file-type@16.5.4: + dependencies: + readable-web-to-node-stream: 3.0.4 + strtok3: 6.3.0 + token-types: 4.2.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.53.3 + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + forwarded@0.2.0: {} + + fraction.js@5.3.4: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@6.0.1: {} + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.0.5 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globby@10.0.2: + dependencies: + '@types/glob': 7.2.0 + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.3 + glob: 7.2.3 + ignore: 5.3.2 + merge2: 1.4.1 + slash: 3.0.0 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + gradient-string@2.0.2: + dependencies: + chalk: 4.1.2 + tinygradient: 1.1.5 + + handlebars@4.7.8: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@3.0.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + header-case@1.0.1: + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + human-signals@2.1.0: {} + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + indent-string@4.0.0: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + inquirer@7.3.3: + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + external-editor: 3.1.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + run-async: 2.4.1 + rxjs: 6.6.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + + inquirer@8.2.7(@types/node@22.19.1): + dependencies: + '@inquirer/external-editor': 1.0.3(@types/node@22.19.1) + ansi-escapes: 4.3.2 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-width: 3.0.0 + figures: 3.2.0 + lodash: 4.17.21 + mute-stream: 0.0.8 + ora: 5.4.1 + run-async: 2.4.1 + rxjs: 7.8.2 + string-width: 4.2.3 + strip-ansi: 6.0.1 + through: 2.3.8 + wrap-ansi: 6.2.0 + transitivePeerDependencies: + - '@types/node' + + iota-array@1.0.0: {} + + ip-address@10.1.0: {} + + ipaddr.js@1.9.1: {} + + is-arrayish@0.3.4: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-buffer@1.1.6: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-lower-case@1.1.3: + dependencies: + lower-case: 1.1.4 + + is-number@7.0.0: {} + + is-path-cwd@2.2.0: {} + + is-path-inside@3.0.3: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + is-upper-case@1.1.2: + dependencies: + upper-case: 1.1.3 + + isbinaryfile@4.0.10: {} + + isexe@2.0.0: {} + + jiti@1.21.7: {} + + joycon@3.1.1: {} + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-schema-traverse@1.0.0: {} + + json-schema@0.4.0: {} + + jsondiffpatch@0.6.0: + dependencies: + '@types/diff-match-patch': 1.0.36 + chalk: 5.6.2 + diff-match-patch: 1.0.5 + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + lilconfig@2.1.0: {} + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + lodash.get@4.4.2: {} + + lodash@4.17.21: {} + + log-symbols@3.0.0: + dependencies: + chalk: 2.4.2 + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lower-case-first@1.0.2: + dependencies: + lower-case: 1.1.4 + + lower-case@1.1.4: {} + + lru-cache@7.18.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@1.0.3: {} + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + methods@1.1.2: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + mimic-fn@2.1.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + ms@2.0.0: {} + + ms@2.1.3: {} + + mute-stream@0.0.8: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + ndarray-ops@1.2.2: + dependencies: + cwise-compiler: 1.1.3 + + ndarray-pixels@4.1.0: + dependencies: + '@types/ndarray': 1.0.14 + ndarray: 1.0.19 + ndarray-ops: 1.2.2 + sharp: 0.33.5 + + ndarray@1.0.19: + dependencies: + iota-array: 1.0.0 + is-buffer: 1.1.6 + + negotiator@0.6.3: {} + + negotiator@1.0.0: {} + + neo-async@2.6.2: {} + + netmask@2.0.2: {} + + no-case@2.3.2: + dependencies: + lower-case: 1.1.4 + + node-plop@0.26.3: + dependencies: + '@babel/runtime-corejs3': 7.28.4 + '@types/inquirer': 6.5.0 + change-case: 3.1.0 + del: 5.1.0 + globby: 10.0.2 + handlebars: 4.7.8 + inquirer: 7.3.3 + isbinaryfile: 4.0.10 + lodash.get: 4.4.2 + mkdirp: 0.5.6 + resolve: 1.22.11 + + node-releases@2.0.27: {} + + normalize-path@3.0.0: {} + + normalize-range@0.1.2: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + object-assign@4.1.1: {} + + object-hash@3.0.0: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + openai@6.9.1(zod@3.25.76): + optionalDependencies: + zod: 3.25.76 + + ora@4.1.1: + dependencies: + chalk: 3.0.0 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + log-symbols: 3.0.0 + mute-stream: 0.0.8 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + os-tmpdir@1.0.2: {} + + p-map@3.0.0: + dependencies: + aggregate-error: 3.1.0 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.0.2 + + param-case@2.1.1: + dependencies: + no-case: 2.3.2 + + parseurl@1.3.3: {} + + pascal-case@2.0.1: + dependencies: + camel-case: 3.0.0 + upper-case-first: 1.1.2 + + path-case@2.1.1: + dependencies: + no-case: 2.3.2 + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-to-regexp@0.1.12: {} + + path-to-regexp@8.3.0: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + peek-readable@4.1.0: {} + + picocolors@1.0.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + pirates@4.0.7: {} + + pkce-challenge@5.0.1: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + postcss-import@15.1.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.11 + + postcss-js@4.1.0(postcss@8.5.6): + dependencies: + camelcase-css: 2.0.1 + postcss: 8.5.6 + + postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.1 + optionalDependencies: + postcss: 8.5.6 + ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 1.21.7 + postcss: 8.5.6 + tsx: 4.20.6 + yaml: 2.8.1 + + postcss-nested@6.2.0(postcss@8.5.6): + dependencies: + postcss: 8.5.6 + postcss-selector-parser: 6.1.2 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss-value-parser@4.2.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + process@0.11.10: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react@19.2.0: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + + readable-web-to-node-stream@3.0.4: + dependencies: + readable-stream: 4.7.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + readdirp@4.1.2: {} + + registry-auth-token@3.3.2: + dependencies: + rc: 1.2.8 + safe-buffer: 5.2.1 + + registry-url@3.1.0: + dependencies: + rc: 1.2.8 + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + reusify@1.1.0: {} + + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + run-async@2.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@6.6.7: + dependencies: + tslib: 1.14.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + secure-json-parse@2.7.0: {} + + semver@7.6.2: {} + + semver@7.7.3: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + send@1.2.0: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + sentence-case@2.1.1: + dependencies: + no-case: 2.3.2 + upper-case-first: 1.1.2 + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.1.2 + semver: 7.7.3 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + + slash@3.0.0: {} + + smart-buffer@4.2.0: {} + + snake-case@2.1.0: + dependencies: + no-case: 2.3.2 + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.7 + transitivePeerDependencies: + - supports-color + + socks@2.8.7: + dependencies: + ip-address: 10.1.0 + smart-buffer: 4.2.0 + + source-map-js@1.2.1: {} + + source-map@0.6.1: {} + + source-map@0.7.6: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-final-newline@2.0.0: {} + + strip-json-comments@2.0.1: {} + + strtok3@6.3.0: + dependencies: + '@tokenizer/token': 0.3.0 + peek-readable: 4.1.0 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + swap-case@1.1.2: + dependencies: + lower-case: 1.1.4 + upper-case: 1.1.3 + + swr@2.3.7(react@19.2.0): + dependencies: + dequal: 2.0.3 + react: 19.2.0 + use-sync-external-store: 1.6.0(react@19.2.0) + + tailwindcss-animate@1.0.7(tailwindcss@3.3.5(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))): + dependencies: + tailwindcss: 3.3.5(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + + tailwindcss@3.3.5(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 2.1.0 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-import: 15.1.0(postcss@8.5.6) + postcss-js: 4.1.0(postcss@8.5.6) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + postcss-nested: 6.2.0(postcss@8.5.6) + postcss-selector-parser: 6.1.2 + resolve: 1.22.11 + sucrase: 3.35.1 + transitivePeerDependencies: + - ts-node + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + throttleit@2.1.0: {} + + through@2.3.8: {} + + tinycolor2@1.6.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinygradient@1.1.5: + dependencies: + '@types/tinycolor2': 1.4.6 + tinycolor2: 1.6.0 + + title-case@2.1.1: + dependencies: + no-case: 2.3.2 + upper-case: 1.1.3 + + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toidentifier@1.0.1: {} + + token-types@4.2.1: + dependencies: + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.19.1 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tsup@8.5.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.0) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.0 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.6)(tsx@4.20.6)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.53.3 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + tsx@4.20.6: + dependencies: + esbuild: 0.25.12 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + turbo-darwin-64@2.6.1: + optional: true + + turbo-darwin-arm64@2.6.1: + optional: true + + turbo-linux-64@2.6.1: + optional: true + + turbo-linux-arm64@2.6.1: + optional: true + + turbo-windows-64@2.6.1: + optional: true + + turbo-windows-arm64@2.6.1: + optional: true + + turbo@2.6.1: + optionalDependencies: + turbo-darwin-64: 2.6.1 + turbo-darwin-arm64: 2.6.1 + turbo-linux-64: 2.6.1 + turbo-linux-arm64: 2.6.1 + turbo-windows-64: 2.6.1 + turbo-windows-arm64: 2.6.1 + + type-fest@0.21.3: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + ufo@1.6.1: {} + + uglify-js@3.19.3: + optional: true + + undici-types@6.21.0: {} + + uniq@1.0.1: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + update-browserslist-db@1.1.4(browserslist@4.28.0): + dependencies: + browserslist: 4.28.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + update-check@1.5.4: + dependencies: + registry-auth-token: 3.3.2 + registry-url: 3.1.0 + + upper-case-first@1.1.2: + dependencies: + upper-case: 1.1.3 + + upper-case@1.1.3: {} + + use-sync-external-store@1.6.0(react@19.2.0): + dependencies: + react: 19.2.0 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + validate-npm-package-name@5.0.1: {} + + vary@1.1.2: {} + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + yaml@2.8.1: {} + + yn@3.1.1: {} + + zod-to-json-schema@3.25.0(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c097372de79566e0cd712ae7d325e99fa609f274 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +packages: + - "apps/*" + - "packages/*" + - "tooling/*" diff --git a/scripts/deploy-chatgpt-mcp.sh b/scripts/deploy-chatgpt-mcp.sh new file mode 100755 index 0000000000000000000000000000000000000000..fff9ba15ff0b51d500f04073d99748279d2c14f1 --- /dev/null +++ b/scripts/deploy-chatgpt-mcp.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# Deploy ChatGPT MCP Server to Hugging Face Spaces +# This is a SEPARATE space just for the MCP server +# Usage: ./scripts/deploy-chatgpt-mcp.sh + +set -e + +HF_SPACE="${HF_SPACE:-MCP-1st-Birthday/eu-ai-act-chatgpt-mcp}" + +echo "🚀 Deploying ChatGPT MCP Server to HF Spaces: $HF_SPACE" + +# Derive PUBLIC_URL from HF_SPACE +PUBLIC_URL="https://$(echo "$HF_SPACE" | tr '[:upper:]' '[:lower:]' | tr '/' '-').hf.space" +MCP_URL="${PUBLIC_URL}/gradio_api/mcp/" +echo "📡 Public URL: $PUBLIC_URL" +echo "🔗 MCP URL: $MCP_URL" + +# Check HF CLI +if ! command -v huggingface-cli &> /dev/null; then + echo "Installing huggingface_hub..." + pip install huggingface_hub +fi + +# Login check +huggingface-cli whoami || { echo "❌ Run: huggingface-cli login"; exit 1; } + +# Create temp dir +TEMP_DIR=$(mktemp -d) +echo "📦 Preparing deployment in $TEMP_DIR" + +# Get repo root directory +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Copy everything except node_modules, .git, dist, .venv +rsync -av --exclude='node_modules' --exclude='.git' --exclude='dist' --exclude='.venv' --exclude='.turbo' "$REPO_ROOT/" "$TEMP_DIR/" +cd "$TEMP_DIR" + +# Use the ChatGPT MCP Dockerfile +cp apps/eu-ai-act-agent/Dockerfile.chatgpt-mcp ./Dockerfile + +# Update PUBLIC_URL in Dockerfile +sed -i.bak "s|PUBLIC_URL=https://.*\.hf\.space|PUBLIC_URL=$PUBLIC_URL|g" Dockerfile +rm -f Dockerfile.bak +echo "✅ Updated PUBLIC_URL in Dockerfile" + +# Create a simple README for this space +cat > README.md << 'EOF' +--- +title: EU AI Act - ChatGPT MCP Server by legitima.ai +emoji: ⚖️ +colorFrom: blue +colorTo: indigo +sdk: docker +pinned: false +tags: + - building-mcp-track-enterprise + - mcp-in-action-track-enterprise +short_description: MCP Server for ChatGPT Apps - EU AI Act Compliance Tools +--- + +# 🇪🇺 EU AI Act - ChatGPT MCP Server by [legitima.ai](https://legitima.ai/mcp-hackathon) powered by [decode](https://decode.gr/en) + +<div align="center"> + <img src="https://www.legitima.ai/mcp-hackathon.png" alt="Gradio MCP Hackathon - EU AI Act Compliance" width="800"/> +</div> + +This is the **MCP Server** for integrating EU AI Act compliance tools with **ChatGPT Desktop**. + +## 🔗 MCP URL + +``` +EOF + +echo "${MCP_URL}" >> README.md + +cat >> README.md << 'EOF' +``` + +## 📖 How to Use in ChatGPT + +1. **Enable Developer Mode** in ChatGPT: Settings → Apps & Connectors → Advanced settings +2. **Create a Connector** with the MCP URL above (choose "No authentication") +3. **Chat with ChatGPT** using `@eu-ai-act` to access the tools + +## 🔧 Available MCP Tools + +| Tool | Description | +|------|-------------| +| `discover_organization` | Research and profile an organization for compliance | +| `discover_ai_services` | Discover and classify AI systems by risk level | +| `assess_compliance` | Generate compliance assessment and documentation | + +## 🤖 Main Agent UI + +For the full interactive chat experience, visit: +**[EU AI Act Compliance Agent](https://huggingface.co/spaces/MCP-1st-Birthday/eu-ai-act-compliance-agent)** + +--- + +Built for the **MCP 1st Birthday Hackathon** 🎂 + +**🔗 Demo & Showcase:** [www.legitima.ai/mcp-hackathon](https://www.legitima.ai/mcp-hackathon) +**📹 Video:** [Guiddes](https://app.guidde.com/share/playlists/2wXbDrSm2YY7YnWMJbftuu?origin=wywDANMIvNhPu9kYVOXCPpdFcya2) +**📱 Social Media:** [LinkedIn Post 1](https://www.linkedin.com/posts/iordanis-sarafidis_mcp-1st-birthday-mcp-1st-birthday-activity-7400132272282144768-ZIir?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs) + +[LinkedIn Post 2](https://www.linkedin.com/posts/billdrosatos_mcp-1st-birthday-mcp-1st-birthday-activity-7400135422502252544-C5BS?utm_source=share&utm_medium=member_desktop&rcm=ACoAAB0ARLABGvUO6Q--hJP0cDG7h0LZT0-roLs) +EOF + +echo "✅ Created README.md with MCP URL" + +# Remove git and push to HF +rm -rf .git +git init -b main +git add -A +git commit -m "Deploy ChatGPT MCP Server" + +# Push to HF Space +git remote add hf "https://huggingface.co/spaces/$HF_SPACE" +git push hf main --force + +echo "" +echo "✅ Deployed to: https://huggingface.co/spaces/$HF_SPACE" +echo "" +echo "🔗 MCP URL for ChatGPT:" +echo " $MCP_URL" +echo "" + +# Cleanup +cd - +rm -rf "$TEMP_DIR" + diff --git a/scripts/deploy-hf.sh b/scripts/deploy-hf.sh new file mode 100755 index 0000000000000000000000000000000000000000..64f4c4c03c7fe69e0cf3b53800631de4ec78959e --- /dev/null +++ b/scripts/deploy-hf.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Deploy to Hugging Face Spaces +# Usage: ./scripts/deploy-hf.sh + +set -e + +HF_SPACE="${HF_SPACE:-MCP-1st-Birthday/eu-ai-act-compliance-agent}" + +echo "🚀 Deploying to HF Spaces: $HF_SPACE" + +# Derive PUBLIC_URL from HF_SPACE (org/repo -> org-repo.hf.space) +# Convert to lowercase and replace / with - +PUBLIC_URL="https://$(echo "$HF_SPACE" | tr '[:upper:]' '[:lower:]' | tr '/' '-').hf.space" +echo "📡 Public URL: $PUBLIC_URL" + +# Check HF CLI +if ! command -v huggingface-cli &> /dev/null; then + echo "Installing huggingface_hub..." + pip install huggingface_hub +fi + +# Login check +huggingface-cli whoami || { echo "❌ Run: huggingface-cli login"; exit 1; } + +# Create temp dir with full repo +TEMP_DIR=$(mktemp -d) +echo "📦 Preparing deployment in $TEMP_DIR" + +# Get repo root directory +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Copy everything except node_modules, .git, dist, .venv +rsync -av --exclude='node_modules' --exclude='.git' --exclude='dist' --exclude='.venv' --exclude='.turbo' "$REPO_ROOT/" "$TEMP_DIR/" +cd "$TEMP_DIR" + +# Move Space files to root (from apps/eu-ai-act-agent) +cp apps/eu-ai-act-agent/Dockerfile ./ +cp apps/eu-ai-act-agent/README.md ./ + +# Update PUBLIC_URL in Dockerfile to match the actual HF Space URL +sed -i.bak "s|PUBLIC_URL=https://.*\.hf\.space|PUBLIC_URL=$PUBLIC_URL|g" Dockerfile +rm -f Dockerfile.bak +echo "✅ Updated PUBLIC_URL in Dockerfile" + +# Remove git and push to HF +rm -rf .git +git init -b main +git add -A +git commit -m "Deploy" + +# Push to HF Space +git remote add hf "https://huggingface.co/spaces/$HF_SPACE" +git push hf main --force + +echo "✅ Deployed: https://huggingface.co/spaces/$HF_SPACE" + +# Cleanup +cd - +rm -rf "$TEMP_DIR" diff --git a/tooling/github/package.json b/tooling/github/package.json new file mode 100644 index 0000000000000000000000000000000000000000..7d40eb459427b3d649fe334f7ab83450689fa4e4 --- /dev/null +++ b/tooling/github/package.json @@ -0,0 +1,3 @@ +{ + "name": "@decode/github" +} diff --git a/tooling/github/setup/action.yml b/tooling/github/setup/action.yml new file mode 100644 index 0000000000000000000000000000000000000000..9ef92c4436d7106293530ca31ff4f8324e2b23f6 --- /dev/null +++ b/tooling/github/setup/action.yml @@ -0,0 +1,17 @@ +name: "Setup and install" +description: "Common setup steps for Actions" + +runs: + using: composite + steps: + - uses: pnpm/action-setup@v2 + - uses: actions/setup-node@v4 + with: + node-version: 18 + cache: "pnpm" + + - shell: bash + run: pnpm add -g turbo + + - shell: bash + run: pnpm install diff --git a/tooling/tailwind/biome.json b/tooling/tailwind/biome.json new file mode 100644 index 0000000000000000000000000000000000000000..db8f245714dad9ac54ffab0fb57ea921e9a650e0 --- /dev/null +++ b/tooling/tailwind/biome.json @@ -0,0 +1,20 @@ +{ + "root": false, + "extends": ["../../biome.json"], + "linter": { + "rules": { + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } + } + } +} diff --git a/tooling/tailwind/index.ts b/tooling/tailwind/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ff7d39836890d7bd8fcc21e9c5a9c858897818b --- /dev/null +++ b/tooling/tailwind/index.ts @@ -0,0 +1,75 @@ +import type { Config } from "tailwindcss"; + +export default { + darkMode: ["class"], + content: [], + future: { + hoverOnlyWhenSupported: true, + }, + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; diff --git a/tooling/tailwind/package.json b/tooling/tailwind/package.json new file mode 100644 index 0000000000000000000000000000000000000000..a5ac2676a338329df93a316c55b07840dab32415 --- /dev/null +++ b/tooling/tailwind/package.json @@ -0,0 +1,25 @@ +{ + "name": "@decode/tailwind-config", + "version": "0.1.0", + "private": true, + "main": "index.ts", + "files": [ + "index.ts" + ], + "scripts": { + "clean": "rm -rf .turbo node_modules", + "lint": "biome lint .", + "format": "biome format . ", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "autoprefixer": "^10.4.16", + "tailwindcss": "3.3.5", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@decode/tsconfig": "workspace:^0.1.0", + "@biomejs/biome": "^1.5.3", + "typescript": "^5.2.2" + } +} diff --git a/tooling/tailwind/tsconfig.json b/tooling/tailwind/tsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..c7075ddae24db860a21cd185675bbba13cbb8d2e --- /dev/null +++ b/tooling/tailwind/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@decode/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["."], + "exclude": ["node_modules"] +} diff --git a/tooling/typescript/base.json b/tooling/typescript/base.json new file mode 100644 index 0000000000000000000000000000000000000000..bf34b7019c13691056c4d8ccefe1cdf89f79d1a1 --- /dev/null +++ b/tooling/typescript/base.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "lib": [ + "dom", + "dom.iterable", + "ES2022" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "jsx": "preserve", + "incremental": true, + "composite": true, + "noUncheckedIndexedAccess": true + }, + "exclude": [ + "node_modules", + "build", + "dist", + ".next", + ".expo" + ] +} \ No newline at end of file diff --git a/tooling/typescript/package.json b/tooling/typescript/package.json new file mode 100644 index 0000000000000000000000000000000000000000..8c24b6b3ce96196e96d9ebc666f1216b18dc9433 --- /dev/null +++ b/tooling/typescript/package.json @@ -0,0 +1,8 @@ +{ + "name": "@decode/tsconfig", + "private": true, + "version": "0.1.0", + "files": [ + "base.json" + ] +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 0000000000000000000000000000000000000000..bf64493c88e581ca2e4e1ef305db191df6677cd3 --- /dev/null +++ b/turbo.json @@ -0,0 +1,72 @@ +{ + "$schema": "https://turborepo.org/schema.json", + "globalDependencies": [ + "**/.env" + ], + "globalEnv": [ + "DATABASE_URL", + "AUTH_DISCORD_ID", + "AUTH_DISCORD_SECRET", + "AUTH_REDIRECT_PROXY_URL", + "AUTH_SECRET", + "AUTH_URL" + ], + "tasks": { + "topo": { + "dependsOn": [ + "^topo" + ] + }, + "build": { + "dependsOn": [ + "^build" + ], + "outputs": [ + ".next/**", + "!.next/cache/**", + "next-env.d.ts", + ".expo/**", + ".output/**", + ".vercel/output/**" + ] + }, + "dev": { + "persistent": true, + "cache": false + }, + "lint": { + "dependsOn": [ + "^topo" + ] + }, + "format": { + "dependsOn": [ + "^topo" + ] + }, + "lint:fix": { + "dependsOn": [ + "^topo" + ] + }, + "format:fix": { + "dependsOn": [ + "^topo" + ] + }, + "typecheck": { + "dependsOn": [ + "^topo" + ], + "outputs": [ + "node_modules/.cache/tsbuildinfo.json" + ] + }, + "clean": { + "cache": false + }, + "//#clean": { + "cache": false + } + } +} diff --git a/turbo/generators/config.ts b/turbo/generators/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..0cbb901ca29c641b6d21a0e1677aa3e1389ab43c --- /dev/null +++ b/turbo/generators/config.ts @@ -0,0 +1,95 @@ +import { execSync } from "node:child_process"; +import type { PlopTypes } from "@turbo/gen"; + +interface PackageJson { + name: string; + scripts: Record<string, string>; + dependencies: Record<string, string>; + devDependencies: Record<string, string>; +} + +export default function generator(plop: PlopTypes.NodePlopAPI): void { + plop.setGenerator("init", { + description: "Generate a new package for the Acme Monorepo", + prompts: [ + { + type: "input", + name: "name", + message: + "What is the name of the package? (You can skip the `@decode/` prefix)", + }, + { + type: "input", + name: "deps", + message: + "Enter a space separated list of dependencies you would like to install", + }, + ], + actions: [ + (answers) => { + if ("name" in answers && typeof answers.name === "string") { + if (answers.name.startsWith("@decode/")) { + answers.name = answers.name.replace("@decode/", ""); + } + } + return "Config sanitized"; + }, + { + type: "add", + path: "packages/{{ name }}/package.json", + templateFile: "templates/package.json.hbs", + }, + { + type: "add", + path: "packages/{{ name }}/tsconfig.json", + templateFile: "templates/tsconfig.json.hbs", + }, + { + type: "add", + path: "packages/{{ name }}/index.ts", + template: "export * from './src';", + }, + { + type: "add", + path: "packages/{{ name }}/src/index.ts", + template: "export const name = '{{ name }}';", + }, + { + type: "modify", + path: "packages/{{ name }}/package.json", + async transform(content, answers) { + if ("deps" in answers && typeof answers.deps === "string") { + const pkg = JSON.parse(content) as PackageJson; + for (const dep of answers.deps.split(" ").filter(Boolean)) { + const version = await fetch( + `https://registry.npmjs.org/-/package/${dep}/dist-tags`, + ) + .then((res) => res.json()) + .then((json) => json.latest); + if (!pkg.dependencies) pkg.dependencies = {}; + pkg.dependencies[dep] = `^${version}`; + } + return JSON.stringify(pkg, null, 2); + } + return content; + }, + }, + async (answers) => { + /** + * Install deps and format everything + */ + if ("name" in answers && typeof answers.name === "string") { + // execSync("pnpm dlx sherif@latest --fix", { + // stdio: "inherit", + // }); + execSync("pnpm i", { stdio: "inherit" }); + execSync( + `pnpm @biomejs/biome format --write packages/${answers.name}/**`, + ); + return "Package formated"; + } + return "Package not formated"; + }, + ], + }); +} diff --git a/turbo/generators/templates/package.json.hbs b/turbo/generators/templates/package.json.hbs new file mode 100644 index 0000000000000000000000000000000000000000..3dde8e833994c5b45b830b4bcc25fd2d75f656a8 --- /dev/null +++ b/turbo/generators/templates/package.json.hbs @@ -0,0 +1,25 @@ +{ + "name": "@decode/{{ name }}", + "private": true, + "version": "0.1.0", + "exports": { + ".": "./index.ts" + }, + "typesVersions": { + "*": { + "*": [ + "src/*" + ] + } + }, + "scripts": { + "clean": "rm -rf .turbo node_modules", + "lint": "biome lint .", + "format": "biome format . ", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@decode/tsconfig": "workspace:^0.1.0", + "typescript": "^5.2.2" + } +} diff --git a/turbo/generators/templates/tsconfig.json.hbs b/turbo/generators/templates/tsconfig.json.hbs new file mode 100644 index 0000000000000000000000000000000000000000..f05562176abd84356dad1b58401c724e4c4dde25 --- /dev/null +++ b/turbo/generators/templates/tsconfig.json.hbs @@ -0,0 +1,8 @@ +{ + "extends": "@decode/tsconfig/base.json", + "compilerOptions": { + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["*.ts", "src"], + "exclude": ["node_modules"] +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000000000000000000000000000000000000..7ae9a3de54d117b5506c69083a44d41b2ef37b69 --- /dev/null +++ b/vercel.json @@ -0,0 +1,5 @@ +{ + "github": { + "silent": true + } +}